From 25256ec417c8a4a2c921638c5370aea942cffd29 Mon Sep 17 00:00:00 2001 From: JunHo Kim Date: Thu, 2 Jul 2026 17:31:43 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20initial=20commit=20=E2=80=94=20portabl?= =?UTF-8?q?e=20agent=20memory=20&=20skill=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted and cleaned up from a prior WSL/Windows agent-memory setup. Contains only project-agnostic content: - Claude_Memory/: feedback rules, user profile, reusable reference docs, session scratch space (claude_info/, claude_log/), mirrored skill defs - caveman/: ultra-compressed communication mode skill + subcommands - html-slide-deck/: self-contained HTML slide deck builder skill Project-specific knowledge (previously mixed in) was moved out to each project's own repository as part of this cleanup. --- .gitignore | 2 + Claude_Memory/MEMORY.md | 46 + Claude_Memory/claude_info/CHECKPOINT.md | 21 + Claude_Memory/claude_info/NOTES.md | 18 + Claude_Memory/claude_info/TIPS.md | 22 + Claude_Memory/claude_info/TODO.md | 15 + Claude_Memory/claude_log/README.md | 51 + .../claude_skills/archive-session/SKILL.md | 59 + .../claude_skills/graphify/.graphify_version | 1 + Claude_Memory/claude_skills/graphify/SKILL.md | 1090 +++++++++++++++++ Claude_Memory/claude_skills/grill-me/SKILL.md | 10 + Claude_Memory/claude_skills/handoff/SKILL.md | 13 + Claude_Memory/feedback_agent_permission.md | 13 + .../feedback_bash_python_auto_approve.md | 11 + Claude_Memory/feedback_caveman_korean.md | 14 + Claude_Memory/feedback_caveman_md_style.md | 14 + Claude_Memory/feedback_handoff_load.md | 15 + .../feedback_linter_before_commit.md | 17 + Claude_Memory/feedback_truncated_output.md | 14 + Claude_Memory/feedback_worktree_check.md | 16 + Claude_Memory/reference_graphify.md | 88 ++ Claude_Memory/user_code_comment_style.md | 16 + Claude_Memory/user_developer_profile.md | 27 + Claude_Memory/user_session_start_skills.md | 11 + Claude_Memory/user_usage_alarm.md | 16 + LICENSE | 21 + README.md | 46 + caveman/cavecrew/README.md | 41 + caveman/cavecrew/SKILL.md | 82 ++ caveman/caveman-commit/README.md | 44 + caveman/caveman-commit/SKILL.md | 65 + caveman/caveman-compress/README.md | 163 +++ caveman/caveman-compress/SECURITY.md | 31 + caveman/caveman-compress/SKILL.md | 111 ++ caveman/caveman-compress/scripts/__init__.py | 9 + caveman/caveman-compress/scripts/__main__.py | 3 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 406 bytes .../__pycache__/__main__.cpython-312.pyc | Bin 0 -> 214 bytes .../scripts/__pycache__/cli.cpython-312.pyc | Bin 0 -> 3055 bytes .../__pycache__/compress.cpython-312.pyc | Bin 0 -> 9999 bytes .../__pycache__/detect.cpython-312.pyc | Bin 0 -> 5834 bytes .../__pycache__/validate.cpython-312.pyc | Bin 0 -> 9467 bytes caveman/caveman-compress/scripts/benchmark.py | 80 ++ caveman/caveman-compress/scripts/cli.py | 85 ++ caveman/caveman-compress/scripts/compress.py | 254 ++++ caveman/caveman-compress/scripts/detect.py | 121 ++ caveman/caveman-compress/scripts/validate.py | 213 ++++ caveman/caveman-help/README.md | 38 + caveman/caveman-help/SKILL.md | 59 + caveman/caveman-review/README.md | 33 + caveman/caveman-review/SKILL.md | 55 + caveman/caveman-stats/README.md | 30 + caveman/caveman-stats/SKILL.md | 10 + caveman/caveman/README.md | 48 + caveman/caveman/SKILL.md | 74 ++ caveman/find-skills/SKILL.md | 142 +++ html-slide-deck.skill | Bin 0 -> 14538 bytes html-slide-deck/SKILL.md | 97 ++ html-slide-deck/references/engine.md | 498 ++++++++ html-slide-deck/references/layout-patterns.md | 224 ++++ html-slide-deck/references/slide-types.md | 134 ++ 61 files changed, 4431 insertions(+) create mode 100755 .gitignore create mode 100755 Claude_Memory/MEMORY.md create mode 100755 Claude_Memory/claude_info/CHECKPOINT.md create mode 100755 Claude_Memory/claude_info/NOTES.md create mode 100755 Claude_Memory/claude_info/TIPS.md create mode 100755 Claude_Memory/claude_info/TODO.md create mode 100755 Claude_Memory/claude_log/README.md create mode 100755 Claude_Memory/claude_skills/archive-session/SKILL.md create mode 100755 Claude_Memory/claude_skills/graphify/.graphify_version create mode 100755 Claude_Memory/claude_skills/graphify/SKILL.md create mode 100755 Claude_Memory/claude_skills/grill-me/SKILL.md create mode 100755 Claude_Memory/claude_skills/handoff/SKILL.md create mode 100755 Claude_Memory/feedback_agent_permission.md create mode 100755 Claude_Memory/feedback_bash_python_auto_approve.md create mode 100755 Claude_Memory/feedback_caveman_korean.md create mode 100755 Claude_Memory/feedback_caveman_md_style.md create mode 100755 Claude_Memory/feedback_handoff_load.md create mode 100755 Claude_Memory/feedback_linter_before_commit.md create mode 100755 Claude_Memory/feedback_truncated_output.md create mode 100755 Claude_Memory/feedback_worktree_check.md create mode 100755 Claude_Memory/reference_graphify.md create mode 100755 Claude_Memory/user_code_comment_style.md create mode 100755 Claude_Memory/user_developer_profile.md create mode 100755 Claude_Memory/user_session_start_skills.md create mode 100755 Claude_Memory/user_usage_alarm.md create mode 100755 LICENSE create mode 100755 README.md create mode 100755 caveman/cavecrew/README.md create mode 100755 caveman/cavecrew/SKILL.md create mode 100755 caveman/caveman-commit/README.md create mode 100755 caveman/caveman-commit/SKILL.md create mode 100755 caveman/caveman-compress/README.md create mode 100755 caveman/caveman-compress/SECURITY.md create mode 100755 caveman/caveman-compress/SKILL.md create mode 100755 caveman/caveman-compress/scripts/__init__.py create mode 100755 caveman/caveman-compress/scripts/__main__.py create mode 100755 caveman/caveman-compress/scripts/__pycache__/__init__.cpython-312.pyc create mode 100755 caveman/caveman-compress/scripts/__pycache__/__main__.cpython-312.pyc create mode 100755 caveman/caveman-compress/scripts/__pycache__/cli.cpython-312.pyc create mode 100755 caveman/caveman-compress/scripts/__pycache__/compress.cpython-312.pyc create mode 100755 caveman/caveman-compress/scripts/__pycache__/detect.cpython-312.pyc create mode 100755 caveman/caveman-compress/scripts/__pycache__/validate.cpython-312.pyc create mode 100755 caveman/caveman-compress/scripts/benchmark.py create mode 100755 caveman/caveman-compress/scripts/cli.py create mode 100755 caveman/caveman-compress/scripts/compress.py create mode 100755 caveman/caveman-compress/scripts/detect.py create mode 100755 caveman/caveman-compress/scripts/validate.py create mode 100755 caveman/caveman-help/README.md create mode 100755 caveman/caveman-help/SKILL.md create mode 100755 caveman/caveman-review/README.md create mode 100755 caveman/caveman-review/SKILL.md create mode 100755 caveman/caveman-stats/README.md create mode 100755 caveman/caveman-stats/SKILL.md create mode 100755 caveman/caveman/README.md create mode 100755 caveman/caveman/SKILL.md create mode 100755 caveman/find-skills/SKILL.md create mode 100755 html-slide-deck.skill create mode 100755 html-slide-deck/SKILL.md create mode 100755 html-slide-deck/references/engine.md create mode 100755 html-slide-deck/references/layout-patterns.md create mode 100755 html-slide-deck/references/slide-types.md diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..d0185e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cache +graphify-out/ diff --git a/Claude_Memory/MEMORY.md b/Claude_Memory/MEMORY.md new file mode 100755 index 0000000..79ab0c9 --- /dev/null +++ b/Claude_Memory/MEMORY.md @@ -0,0 +1,46 @@ +# Memory Index + +> 작업 시작 시 `claude_info/CHECKPOINT.md` 먼저 확인. +> 소스 코드 작업 전 `graphify-out/GRAPH_REPORT.md` 먼저 읽기. + +## 세션 상태 +- [CHECKPOINT](claude_info/CHECKPOINT.md) — 현재 작업 상태, 브랜치, 다음 시작 위치 +- [NOTES](claude_info/NOTES.md) — 세션별 주요 발견사항·비직관적 동작 +- [TIPS](claude_info/TIPS.md) — 재사용 패턴 & 해결법 +- [TODO](claude_info/TODO.md) — 우선순위별 다음 작업 목록 + +## 작업 로그 (claude_log/) +- (현재 없음 — 프로젝트별 로그는 각 프로젝트 폴더에 기록. 포맷은 [README](claude_log/README.md) 참고) + +## Feedback (행동 규칙) +- [Commit 전 Linter](feedback_linter_before_commit.md) — commit 전 변경 파일 linter 실행 +- [Agent 자동 승인](feedback_agent_permission.md) — Agent(*) 자동 실행 +- [Bash/Python3 자동 승인](feedback_bash_python_auto_approve.md) — Bash/Python3 즉시 실행 +- [Truncated output 판단 금지](feedback_truncated_output.md) — "Output too large" 시 전체 파일 읽고 판단 +- [Caveman 한국어](feedback_caveman_korean.md) — caveman 모드 활성 시 한국어 파편문 +- [Caveman MD 스타일](feedback_caveman_md_style.md) — 기획/스펙 문서 caveman style 작성 +- [Handoff 로드](feedback_handoff_load.md) — 핸드오프 파일 경유 세션 인계 +- [Worktree 확인](feedback_worktree_check.md) — 코드 작업 전 git worktree list + .worktrees/ 확인 + +## User Profile +- [코드 주석 스타일](user_code_comment_style.md) — 주요 로직 영문 주석, Doxygen (모든 코드베이스 공통) +- [세션 시작 스킬 안내](user_session_start_skills.md) — 세션 시작 시 활성 스킬 목록 제시 +- [Usage 리셋 알람](user_usage_alarm.md) — "usage 꽉 찼다" → 5시간 후 알람 예약 +- [Developer Profile](user_developer_profile.md) — 소통/의사결정/디버깅 성향 프로필 + +## Reference +- [graphify 도구](reference_graphify.md) — Knowledge Graph 빌드, 출력물 구조, 명령어 (범용) + +## Skills +- `claude_skills/` — archive-session, graphify, grill-me, handoff +- `../caveman/` — caveman, cavecrew, caveman-commit/compress/help/review/stats, find-skills +- `../html-slide-deck/`, `../html-slide-deck.skill` + +--- + +## 프로젝트별 메모리는 각 프로젝트 폴더에서 관리 + +이 폴더(`Agent_Memories`)는 프로젝트에 무관하게 재사용 가능한 범용 지식만 보관한다. +특정 프로젝트(코드베이스·언어·툴체인에 종속된) 지식은 해당 프로젝트 폴더 안에서 관리한다. +예: Universal_AVI 프로젝트 관련 지식은 `4_Universal_AVI/.claude/claude_info/`, `claude_log/`에 있음 +(2026-07-01 통합 정리 이전 자료는 `4_Universal_AVI/.claude/_imported_from_agent_memory_unification/`에 아카이브됨). diff --git a/Claude_Memory/claude_info/CHECKPOINT.md b/Claude_Memory/claude_info/CHECKPOINT.md new file mode 100755 index 0000000..6d5d3c6 --- /dev/null +++ b/Claude_Memory/claude_info/CHECKPOINT.md @@ -0,0 +1,21 @@ +# CHECKPOINT — 세션 진행 상황 + +> 작업 시작 시 이 파일을 가장 먼저 확인할 것. +> 작업 종료 전 현재 상태로 업데이트할 것. + +--- + +## 현재 상태 + +- **마지막 업데이트**: (없음) +- **진행 중인 작업**: 없음 +- **브랜치**: (없음) + +--- + + diff --git a/Claude_Memory/claude_info/NOTES.md b/Claude_Memory/claude_info/NOTES.md new file mode 100755 index 0000000..1973be9 --- /dev/null +++ b/Claude_Memory/claude_info/NOTES.md @@ -0,0 +1,18 @@ +# NOTES — 중요 발견사항 + +작업 중 발견한 설계 결정, 비직관적 동작, 주의사항을 기록. + +--- + +## 포맷 + +```markdown +### [YYYY-MM-DD] 제목 + +**관련 파일**: `경로` +**내용**: 발견한 사실 / 결정 이유 / 주의사항 +``` + +--- + + diff --git a/Claude_Memory/claude_info/TIPS.md b/Claude_Memory/claude_info/TIPS.md new file mode 100755 index 0000000..f60b439 --- /dev/null +++ b/Claude_Memory/claude_info/TIPS.md @@ -0,0 +1,22 @@ +# TIPS — 재사용 패턴 & 해결법 + +반복적으로 쓰이는 코드 패턴, 편집 방법, 이슈 해결법을 기록. + +--- + +## 포맷 + +```markdown +### [태그] 제목 + +**상황**: 어떤 상황에서 쓰는가 +**해결책**: 방법 설명 +**예시**: +\`\`\`cpp +// 코드 +\`\`\` +``` + +--- + + diff --git a/Claude_Memory/claude_info/TODO.md b/Claude_Memory/claude_info/TODO.md new file mode 100755 index 0000000..8d792c2 --- /dev/null +++ b/Claude_Memory/claude_info/TODO.md @@ -0,0 +1,15 @@ +# TODO — 다음 작업 목록 + +미완료 항목, 다음 세션에서 이어받을 작업을 우선순위 순으로 기록. + +--- + +## 우선순위 기준 + +- `P1` — 즉시 처리 필요 (블로커) +- `P2` — 현재 마일스톤 내 처리 +- `P3` — 다음 마일스톤 이후 가능 + +--- + + diff --git a/Claude_Memory/claude_log/README.md b/Claude_Memory/claude_log/README.md new file mode 100755 index 0000000..238f3cc --- /dev/null +++ b/Claude_Memory/claude_log/README.md @@ -0,0 +1,51 @@ +# claude_md — 작업 지식 로그 + +작업 중 발견한 코드 구조, 패턴, 제약조건을 날짜/주제별로 기록. +에이전트가 동일 영역 재작업 시 중복 탐색을 줄이기 위한 지식 베이스. + +--- + +## 파일 네이밍 + +``` +LOG_YYYYMMDD_.md +``` + +예시: +- `LOG_20260410_Auth_Module_Structure.md` +- `LOG_20260411_Build_Pipeline_Encoding.md` + +--- + +## 파일 포맷 + +```markdown +--- +date: YYYY-MM-DD +topic: 주제 (한글 가능) +related_files: [파일경로1, 파일경로2] +tags: [태그1, 태그2] +--- + +# 제목 + +## 발견 내용 + +... + +## 주의사항 + +... + +## 관련 코드 위치 + +- `파일경로:라인번호` — 설명 +``` + +--- + +## 저장 시점 + +- 새로운 모듈/클래스 구조를 파악한 후 +- 예상과 다른 동작/패턴을 발견했을 때 +- 향후 동일 영역 작업 시 참조 가치가 있는 정보를 얻었을 때 diff --git a/Claude_Memory/claude_skills/archive-session/SKILL.md b/Claude_Memory/claude_skills/archive-session/SKILL.md new file mode 100755 index 0000000..44ea71b --- /dev/null +++ b/Claude_Memory/claude_skills/archive-session/SKILL.md @@ -0,0 +1,59 @@ +--- +name: archive-session +description: 현재 세션의 작업 결과, 파악된 지식, 다음 단계(TODO)를 프로젝트의 공식 문서 체계(claude_log, claude_info)에 따라 자동으로 아카이브하고 요약합니다. 작업 종료 직전이나 주요 마일스톤 완료 시 사용합니다. +--- + +# Archive Session + +이 스킬은 현재 세션에서 발생한 지식과 진행 상황을 구조화된 문서로 저장하여 다음 세션에서 문맥을 완벽하게 복원할 수 있도록 돕습니다. + +## 📁 문서 체계 (Storage Locations) + +모든 문서는 프로젝트 루트의 `.claude/` 하위에 저장됩니다. + +- **작업 로그 (`.claude/claude_log/`)**: 세션의 상세 기술적 내역 보관. + - 파일명: `LOG_YYYYMMDD_.md` +- **세션 정보 (`.claude/claude_info/`)**: 지속적으로 업데이트되는 상태 정보. + - `CHECKPOINT.md`: 현재 위치 및 다음 시작 지점. + - `NOTES.md`: 파악된 주요 기술적 사실. + - `TIPS.md`: 재사용 가능한 패턴. + - `TODO.md`: 잔여 작업 목록. + +## 🔄 워크플로우 (Workflow) + +스킬 호출 시 다음 단계를 수행하세요: + +### 1. 정보 수집 및 요약 +- `git log` 및 `git diff`를 통해 이번 세션의 코드 변경 사항 확인. +- 파악된 새로운 아키텍처 규칙, 안티 패턴, 파일 구조 정리. + +### 2. 상세 로그 작성 (`.claude/claude_log/`) +새로운 로그 파일을 생성합니다. +- **날짜**: YYYY-MM-DD +- **주제**: 핵심 작업 내용 (예: UI Mapping Enhancement) +- **내용**: 변경된 파일 목록, 구현된 주요 로직, 직면했던 문제와 해결 방법. + +### 3. 상태 업데이트 (`.claude/claude_info/`) +기존 파일들을 **교체(Overwrite)** 하거나 **추가(Append)** 합니다. +- **CHECKPOINT**: 이전 내용을 지우고 현재의 최신 상태와 "Next Step"만 명확히 기술. +- **NOTES/TIPS**: 이번에 새로 배운 내용이 있다면 리스트에 추가. +- **TODO**: 완료된 항목은 제거하거나 체크(`[x]`)하고, 새로 발견된 과제 추가. + +### 4. 프로젝트 루트 업데이트 +- **CLAUDE.md**: 프로젝트 전반에 걸친 중요한 규칙(Rule) 변경이 있다면 반영. + +### 5. Graphify 지식 그래프 갱신 +세션 중 코드 변경이 있었다면 `graphify update .`를 실행하여 지식 그래프를 최신 상태로 유지. +```bash +if command -v graphify &> /dev/null; then + graphify update . +fi +``` +- graphify CLI가 없으면 skip. +- 완료 후 사용자에게 갱신 여부 보고. + +## 💡 작성 가이드라인 (Writing Guidelines) + +- **언어**: 모든 문서는 **한국어**로 작성하되, 기술 용어는 원문을 병기하거나 그대로 사용 (예: Multi-line support). +- **간결성**: Caveman 스타일을 유지하여 핵심 정보만 불릿 포인트로 기술. +- **검증**: 아카이브 완료 후 `ls` 명령으로 파일 생성을 확인하고 사용자에게 요약 보고. diff --git a/Claude_Memory/claude_skills/graphify/.graphify_version b/Claude_Memory/claude_skills/graphify/.graphify_version new file mode 100755 index 0000000..c18d72b --- /dev/null +++ b/Claude_Memory/claude_skills/graphify/.graphify_version @@ -0,0 +1 @@ +0.8.1 \ No newline at end of file diff --git a/Claude_Memory/claude_skills/graphify/SKILL.md b/Claude_Memory/claude_skills/graphify/SKILL.md new file mode 100755 index 0000000..7595a46 --- /dev/null +++ b/Claude_Memory/claude_skills/graphify/SKILL.md @@ -0,0 +1,1090 @@ +--- +name: graphify +description: "any input (code, docs, papers, images, videos) to knowledge graph. Use when user asks any question about a codebase, documents, or project content - especially if c:/MyCode/4_SourceTree/SNI_GitHub/Agent_Memories/graphify-out/ exists, treat the question as a /graphify query." +trigger: /graphify +--- + +# /graphify + +Turn any folder of files into a navigable knowledge graph with community detection, an honest audit trail, and three outputs: interactive HTML, GraphRAG-ready JSON, and a plain-language GRAPH_REPORT.md. + +## Usage + +``` +/graphify # full pipeline on current directory → Obsidian vault +/graphify # full pipeline on specific path +/graphify https://github.com// # clone repo then run full pipeline on it +/graphify https://github.com// --branch # clone a specific branch +/graphify ... # clone multiple repos, build each, merge into one cross-repo graph +/graphify --mode deep # thorough extraction, richer INFERRED edges +/graphify --update # incremental - re-extract only new/changed files +/graphify --directed # build directed graph (preserves edge direction: source→target) +/graphify --whisper-model medium # use a larger Whisper model for better transcription accuracy +/graphify --cluster-only # rerun clustering on existing graph +/graphify --no-viz # skip visualization, just report + JSON +/graphify --html # (HTML is generated by default - this flag is a no-op) +/graphify --svg # also export graph.svg (embeds in Notion, GitHub) +/graphify --graphml # export graph.graphml (Gephi, yEd) +/graphify --neo4j # generate graphify-out/cypher.txt for Neo4j +/graphify --neo4j-push bolt://localhost:7687 # push directly to Neo4j +/graphify --mcp # start MCP stdio server for agent access +/graphify --watch # watch folder, auto-rebuild on code changes (no LLM needed) +/graphify --wiki # build agent-crawlable wiki (index.md + one article per community) +/graphify --obsidian --obsidian-dir ~/vaults/my-project # write vault to custom path (e.g. existing vault) +/graphify add # fetch URL, save to ./raw, update graph +/graphify add --author "Name" # tag who wrote it +/graphify add --contributor "Name" # tag who added it to the corpus +/graphify query "" # BFS traversal - broad context +/graphify query "" --dfs # DFS - trace a specific path +/graphify query "" --budget 1500 # cap answer at N tokens +/graphify path "AuthModule" "Database" # shortest path between two concepts +/graphify explain "SwinTransformer" # plain-language explanation of a node +``` + +## What graphify is for + +Drop any folder of code, docs, papers, images, or video into graphify and get a queryable knowledge graph. Persistent across sessions, honest audit trail (EXTRACTED/INFERRED/AMBIGUOUS), community detection surfaces cross-document connections you wouldn't think to ask about. + +## What You Must Do When Invoked + +If the user invoked `/graphify --help` or `/graphify -h` (with no other arguments), print the contents of the `## Usage` section above verbatim and stop. Do not run any commands, do not detect files, do not default the path to `.`. Just print the Usage block and return. + +If no path was given, use `.` (current directory). Do not ask the user for a path. + +If the path argument starts with `https://github.com/` or `http://github.com/`, treat it as a GitHub URL - run Step 0 before anything else, then continue with the resolved local path. + +Follow these steps in order. Do not skip steps. + +### ⚠️ FIXED OUTPUT DIRECTORY (MANDATORY) + +**All graphify outputs MUST go to this fixed absolute path — never a relative `graphify-out/` inside the scanned project directory.** + +| Context | Path | +|---------|------| +| Linux/bash | `/sessions/pensive-quirky-hypatia/mnt/Agent_Memories/graphify-out` | +| Windows | `C:\MyCode\4_SourceTree\SNI_GitHub\Agent_Memories\graphify-out` | + +At the very start of every run, set this variable and create the directory: + +```bash +GRAPHIFY_OUT="/sessions/pensive-quirky-hypatia/mnt/Agent_Memories/graphify-out" +mkdir -p "$GRAPHIFY_OUT" +``` + +In **every bash block** below, replace `graphify-out/` with `$GRAPHIFY_OUT/`. +In **every Python block** below, replace `Path('graphify-out/...)` with `Path('/sessions/pensive-quirky-hypatia/mnt/Agent_Memories/graphify-out/...')`. + +This rule applies to ALL steps and ALL subcommands (`--update`, `query`, `path`, `explain`, `add`, `--cluster-only`, etc.). Do NOT write output files next to the scanned project. + +--- + +### Step 0 - Clone GitHub repo(s) (only if a GitHub URL was given) + +**Single repo:** +```bash +LOCAL_PATH=$(graphify clone [--branch ]) +# Use LOCAL_PATH as the target for all subsequent steps +``` + +**Multiple repos (cross-repo graph):** +```bash +# Clone each repo, run the full pipeline on each, then merge +graphify clone # → ~/.graphify/repos// +graphify clone # → ~/.graphify/repos// +# Run /graphify on each local path to produce their graph.json files +# Then merge: +graphify merge-graphs \ + ~/.graphify/repos///graphify-out/graph.json \ + ~/.graphify/repos///graphify-out/graph.json \ + --out graphify-out/cross-repo-graph.json +``` + +Graphify clones into `~/.graphify/repos//` and reuses existing clones on repeat runs. Each node in the merged graph carries a `repo` attribute so you can filter by origin. + +### Step 1 - Ensure graphify is installed + +```bash +# Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs) +PYTHON="" +GRAPHIFY_BIN=$(which graphify 2>/dev/null) +# 1. uv tool installs — most reliable on modern Mac/Linux +if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then + _UV_PY=$(uv tool run graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null) + if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi +fi +# 2. Read shebang from graphify binary (pipx and direct pip installs) +if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then + _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + case "$_SHEBANG" in + *[!a-zA-Z0-9/_.-]*) ;; + *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;; + esac +fi +# 3. Fall back to python3 +if [ -z "$PYTHON" ]; then PYTHON="python3"; fi +if ! "$PYTHON" -c "import graphify" 2>/dev/null; then + if command -v uv >/dev/null 2>&1; then + uv tool install --upgrade graphifyy -q 2>&1 | tail -3 + _UV_PY=$(uv tool run graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null) + if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi + else + "$PYTHON" -m pip install graphifyy -q 2>/dev/null \ + || "$PYTHON" -m pip install graphifyy -q --break-system-packages 2>&1 | tail -3 + fi +fi +# Write interpreter path for all subsequent steps (persists across invocations) +mkdir -p graphify-out +"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)" +# Save scan root so `graphify update` (no args) knows where to look next time +echo "$(cd INPUT_PATH && pwd)" > graphify-out/.graphify_root +``` + +If the import succeeds, print nothing and move straight to Step 2. + +**In every subsequent bash block, replace `python3` with `$(cat graphify-out/.graphify_python)` to use the correct interpreter.** + +### Step 2 - Detect files + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.detect import detect +from pathlib import Path +result = detect(Path('INPUT_PATH')) +print(json.dumps(result, ensure_ascii=False)) +" > graphify-out/.graphify_detect.json +``` + +Replace INPUT_PATH with the actual path the user provided. Do NOT cat or print the JSON - read it silently and present a clean summary instead: + +``` +Corpus: X files · ~Y words + code: N files (.py .ts .go ...) + docs: N files (.md .txt ...) + papers: N files (.pdf ...) + images: N files + video: N files (.mp4 .mp3 ...) +``` + +Omit any category with 0 files from the summary. + +Then act on it: +- If `total_files` is 0: stop with "No supported files found in [path]." +- If `skipped_sensitive` is non-empty: mention file count skipped, not the file names. +- If `total_words` > 2,000,000 OR `total_files` > 200: show the warning and the top 5 subdirectories by file count, then ask which subfolder to run on. Wait for the user's answer before proceeding. +- Otherwise: proceed directly to Step 2.5 if video files were detected, or Step 3 if not. + +### Step 2.5 - Transcribe video / audio files (only if video files detected) + +Skip this step entirely if `detect` returned zero `video` files. + +Video and audio files cannot be read directly. Transcribe them to text first, then treat the transcripts as doc files in Step 3. + +**Strategy:** Read the god nodes from `graphify-out/.graphify_detect.json` (or the analysis file if it exists from a previous run). You are already a language model — write a one-sentence domain hint yourself from those labels. Then pass it to Whisper as the initial prompt. No separate API call needed. + +**However**, if the corpus has *only* video files and no other docs/code, use the generic fallback prompt: `"Use proper punctuation and paragraph breaks."` + +**Step 1 - Write the Whisper prompt yourself.** + +Read the top god node labels from detect output or analysis, then compose a short domain hint sentence, for example: + +- Labels: `transformer, attention, encoder, decoder` → `"Machine learning research on transformer architectures and attention mechanisms. Use proper punctuation and paragraph breaks."` +- Labels: `kubernetes, deployment, pod, helm` → `"DevOps discussion about Kubernetes deployments and Helm charts. Use proper punctuation and paragraph breaks."` + +Set it as `WHISPER_PROMPT` to use in the next command. + +**Step 2 - Transcribe:** + +```bash +GRAPHIFY_WHISPER_MODEL=base # or whatever --whisper-model the user passed +$(cat graphify-out/.graphify_python) -c " +import json, os +from pathlib import Path +from graphify.transcribe import transcribe_all + +detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\")) +video_files = detect.get('files', {}).get('video', []) +prompt = os.environ.get('GRAPHIFY_WHISPER_PROMPT', 'Use proper punctuation and paragraph breaks.') + +transcript_paths = transcribe_all(video_files, initial_prompt=prompt) +print(json.dumps(transcript_paths, ensure_ascii=False)) +" > graphify-out/.graphify_transcripts.json +``` + +After transcription: +- Read the transcript paths from `graphify-out/.graphify_transcripts.json` +- Add them to the docs list before dispatching semantic subagents in Step 3B +- Print how many transcripts were created: `Transcribed N video file(s) -> treating as docs` +- If transcription fails for a file, print a warning and continue with the rest + +**Whisper model:** Default is `base`. If the user passed `--whisper-model `, set `GRAPHIFY_WHISPER_MODEL=` in the environment before running the command above. + +### Step 3 - Extract entities and relationships + +**Before starting:** note whether `--mode deep` was given. You must pass `DEEP_MODE=true` to every subagent in Step B2 if it was. Track this from the original invocation - do not lose it. + +This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens). + +**Before dispatching subagents:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user: +> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`). + +Print it once, then continue. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching Claude subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it. + +> **No other API keys are read.** If `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, fall straight through to Claude Code subagent dispatch (Part B below) — the host session itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key from the environment. If a host agent prompts the user for `ANTHROPIC_API_KEY` to run extraction, that prompt is a misread of this skill — ignore it and dispatch subagents as written. + +**Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.** + +Note: Parallelizing AST + semantic saves 5-15s on large corpora. AST is deterministic and fast; start it while subagents are processing docs/papers. + +#### Part A - Structural extraction for code files + +For any code files detected, run AST extraction in parallel with Part B subagents: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.extract import collect_files, extract +from pathlib import Path +import json + +code_files = [] +detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\")) +for f in detect.get('files', {}).get('code', []): + code_files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)]) + +if code_files: + result = extract(code_files, cache_root=Path('.')) + Path('graphify-out/.graphify_ast.json').write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding=\"utf-8\") + print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges') +else: + Path('graphify-out/.graphify_ast.json').write_text(json.dumps({'nodes':[],'edges':[],'input_tokens':0,'output_tokens':0}, ensure_ascii=False), encoding=\"utf-8\") + print('No code files - skipping AST extraction') +" +``` + +#### Part B - Semantic extraction (parallel subagents) + +**Fast path:** If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C. AST handles code - there is nothing for semantic subagents to do. + +**MANDATORY: You MUST use the Agent tool here. Reading files yourself one-by-one is forbidden - it is 5-10x slower. If you do not use the Agent tool you are doing this wrong.** + +Before dispatching subagents, print a timing estimate: +- Load `total_words` and file counts from `graphify-out/.graphify_detect.json` +- Estimate agents needed: `ceil(uncached_non_code_files / 22)` (chunk size is 20-25) +- Estimate time: ~45s per agent batch (they run in parallel, so total ≈ 45s × ceil(agents/parallel_limit)) +- Print: "Semantic extraction: ~N files → X agents, estimated ~Ys" + +**Step B0 - Check extraction cache first** + +Before dispatching any subagents, check which files already have cached extraction results: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.cache import check_semantic_cache +from pathlib import Path + +detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\")) +all_files = [f for files in detect['files'].values() for f in files] + +cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(all_files) + +if cached_nodes or cached_edges or cached_hyperedges: + Path('graphify-out/.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges}, ensure_ascii=False), encoding=\"utf-8\") +Path('graphify-out/.graphify_uncached.txt').write_text('\n'.join(uncached), encoding=\"utf-8\") +print(f'Cache: {len(all_files)-len(uncached)} files hit, {len(uncached)} files need extraction') +" +``` + +Only dispatch subagents for files listed in `graphify-out/.graphify_uncached.txt`. If all files are cached, skip to Part C directly. + +**Step B1 - Split into chunks** + +Load files from `graphify-out/.graphify_uncached.txt`. Split into chunks of 20-25 files each. Each image gets its own chunk (vision needs separate context). When splitting, group files from the same directory together so related artifacts land in the same chunk and cross-file relationships are more likely to be extracted. + +**Step B2 - Dispatch ALL subagents in a single message** + +Call the Agent tool multiple times IN THE SAME RESPONSE - one call per chunk. This is the only way they run in parallel. If you make one Agent call, wait, then make another, you are doing it sequentially and defeating the purpose. + +**IMPORTANT - subagent type:** Always use `subagent_type="general-purpose"`. Do NOT use `Explore` - it is read-only and cannot write chunk files to disk, which silently drops extraction results. General-purpose has Write and Bash access which the subagent needs. + +Concrete example for 3 chunks: +``` +[Agent tool call 1: files 1-15, subagent_type="general-purpose"] +[Agent tool call 2: files 16-30, subagent_type="general-purpose"] +[Agent tool call 3: files 31-45, subagent_type="general-purpose"] +``` +All three in one message. Not three separate messages. + +Each subagent receives this exact prompt (substitute FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE, and CHUNK_PATH). + +CHUNK_PATH must be an **absolute** path — derive it before dispatching: +```bash +PROJECT_ROOT=$(cat graphify-out/.graphify_root) +# Then for chunk N: CHUNK_PATH="${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json" +``` + +Subagent prompt template: + +``` +You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment. +Output ONLY valid JSON matching the schema below - no explanation, no markdown fences, no preamble. + +Files (chunk CHUNK_NUM of TOTAL_CHUNKS): +FILE_LIST + +Rules: +- EXTRACTED: relationship explicit in source (import, call, citation, "see §3.2") +- INFERRED: reasonable inference (shared data structure, implied dependency) +- AMBIGUOUS: uncertain - flag for review, do not omit + +Code files: focus on semantic edges AST cannot find (call relationships, shared data, arch patterns). + Do not re-extract imports - AST already has those. +Doc/paper files: extract named concepts, entities, citations. For rationale (WHY decisions were made, trade-offs, design intent): store as a `rationale` attribute on the relevant concept node — do NOT create a separate rationale node or fragment node. Only create a node for something that is itself a named entity or concept. Use `file_type:"rationale"` for concept-like nodes (ideas, principles, mechanisms, design patterns). `file_type` MUST be one of exactly these six values: `code`, `document`, `paper`, `image`, `rationale`, `concept`. Any other value is invalid and will be rejected. +Code files: when adding `calls` edges, source MUST be the caller (the function/class doing the calling), target MUST be the callee. Never reverse this direction. +Image files: use vision to understand what the image IS - do not just OCR. + UI screenshot: layout patterns, design decisions, key elements, purpose. + Chart: metric, trend/insight, data source. + Tweet/post: claim as node, author, concepts mentioned. + Diagram: components and connections. + Research figure: what it demonstrates, method, result. + Handwritten/whiteboard: ideas and arrows, mark uncertain readings AMBIGUOUS. + +DEEP_MODE (if --mode deep was given): be aggressive with INFERRED edges - indirect deps, + shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting. + +Semantic similarity: if two concepts in this chunk solve the same problem or represent the same idea without any structural link (no import, no call, no citation), add a `semantically_similar_to` edge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95). Examples: +- Two functions that both validate user input but never call each other +- A class in code and a concept in a paper that describe the same algorithm +- Two error types that handle the same failure mode differently +Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them for trivially similar things. + +Hyperedges: if 3 or more nodes clearly participate together in a shared concept, flow, or pattern that is not captured by pairwise edges alone, add a hyperedge to a top-level `hyperedges` array. Examples: +- All classes that implement a common protocol or interface +- All functions in an authentication flow (even if they don't all call each other) +- All concepts from a paper section that form one coherent idea +Use sparingly — only when the group relationship adds information beyond the pairwise edges. Maximum 3 hyperedges per chunk. + +If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author, + contributor onto every node from that file. + +confidence_score is REQUIRED on every edge - never omit it, never use 0.5 as a default: +- EXTRACTED edges: confidence_score = 1.0 always +- INFERRED edges: pick exactly ONE value from this set — never 0.5: + 0.95 direct structural evidence (shared data structure, named cross-file reference). + 0.85 strong inference (clear functional alignment, no direct symbol link). + 0.75 reasonable inference (shared problem domain + similar shape, requires interpretation). + 0.65 weak inference (thematically related, no shape evidence). + 0.55 speculative but plausible (surface-level co-occurrence only). + Models follow discrete rubrics better than continuous ranges; the bimodal + distribution observed in production (>50% at 0.5, >40% at 0.85+) shows the + range guidance is being collapsed to a binary. If no value above fits, mark + the edge AMBIGUOUS rather than picking 0.4 or below. +- AMBIGUOUS edges: 0.1-0.3 + +Node ID format: lowercase, only `[a-z0-9_]`, no dots or slashes. Format: `{stem}_{entity}` where stem is `{parent_dir}_{filename_without_ext}` (the **immediate** parent directory name + the filename stem, both lowercased with non-alphanumeric chars replaced by `_`) and entity is the symbol name similarly normalized. Only one level of parent is used — not the full path. Examples: `src/auth/session.py` + `ValidateToken` → `auth_session_validatetoken`; `lib/utils/helpers.py` + `parse_url` → `utils_helpers_parse_url`; `tests/test_foo.py` + `_helper` → `tests_test_foo_helper`. Top-level files (no parent dir, e.g. `setup.py`) use just the filename stem: `setup_my_func`. This must match the ID the AST extractor generates — using just the filename (e.g., `session_validatetoken`) or the full path (e.g., `src_auth_session_validatetoken`) will create orphan ghost-duplicate nodes. If you are re-extracting a project that had ghost duplicates under the old format, the user should run `graphify extract --force` to rebuild cleanly. CRITICAL: never append chunk numbers, sequence numbers, or any suffix to an ID (no `_c1`, `_c2`, `_chunk2`, etc.). IDs must be deterministic from the label alone — the same entity must always produce the same ID regardless of which chunk processes it. + +Generate the extraction JSON matching this schema exactly: +{"nodes":[{"id":"session_validatetoken","label":"Human Readable Name","file_type":"code|document|paper|image|rationale|concept","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"relative/path"}],"input_tokens":0,"output_tokens":0} + +Then write the JSON to disk using the Write tool at this exact absolute path (no relative paths — Write resolves relative paths against an undefined cwd and the file will be silently lost): +CHUNK_PATH +``` + +**Step B3 - Collect, cache, and merge** + +Wait for all subagents. For each result: +- Check that `graphify-out/.graphify_chunk_NN.json` exists on disk — this is the success signal +- If the file exists and contains valid JSON with `nodes` and `edges`, include it and save to cache +- If the file is missing, the subagent was likely dispatched as read-only (Explore type) — print a warning: "chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent." Do not silently skip. +- If a subagent failed or returned invalid JSON, print a warning and skip that chunk - do not abort + +If more than half the chunks failed or are missing, stop and tell the user to re-run and ensure `subagent_type="general-purpose"` is used. + +Merge all chunk files into `.graphify_semantic_new.json`. **After each Agent call completes, read the real token counts from the Agent tool result's `usage` field and write them back into the chunk JSON before merging** — the chunk JSON itself always has placeholder zeros. Then run: +```bash +$(cat graphify-out/.graphify_python) -c " +import json, glob +from pathlib import Path + +chunks = sorted(glob.glob('graphify-out/.graphify_chunk_*.json')) +all_nodes, all_edges, all_hyperedges = [], [], [] +total_in, total_out = 0, 0 +for c in chunks: + d = json.loads(Path(c).read_text(encoding=\"utf-8\")) + all_nodes += d.get('nodes', []) + all_edges += d.get('edges', []) + all_hyperedges += d.get('hyperedges', []) + total_in += d.get('input_tokens', 0) + total_out += d.get('output_tokens', 0) +Path('graphify-out/.graphify_semantic_new.json').write_text(json.dumps({ + 'nodes': all_nodes, 'edges': all_edges, 'hyperedges': all_hyperedges, + 'input_tokens': total_in, 'output_tokens': total_out, +}, indent=2, ensure_ascii=False), encoding=\"utf-8\") +print(f'Merged {len(chunks)} chunks: {total_in:,} in / {total_out:,} out tokens') +" +``` + +Save new results to cache: +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.cache import save_semantic_cache +from pathlib import Path + +new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]} +saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', [])) +print(f'Cached {saved} files') +" +``` + +Merge cached + new results into `graphify-out/.graphify_semantic.json`: +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path + +cached = json.loads(Path('graphify-out/.graphify_cached.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_cached.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]} +new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]} + +all_nodes = cached['nodes'] + new.get('nodes', []) +all_edges = cached['edges'] + new.get('edges', []) +all_hyperedges = cached.get('hyperedges', []) + new.get('hyperedges', []) +seen = set() +deduped = [] +for n in all_nodes: + if n['id'] not in seen: + seen.add(n['id']) + deduped.append(n) + +merged = { + 'nodes': deduped, + 'edges': all_edges, + 'hyperedges': all_hyperedges, + 'input_tokens': new.get('input_tokens', 0), + 'output_tokens': new.get('output_tokens', 0), +} +Path('graphify-out/.graphify_semantic.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\") +print(f'Extraction complete - {len(deduped)} nodes, {len(all_edges)} edges ({len(cached[\"nodes\"])} from cache, {len(new.get(\"nodes\",[]))} new)') +" +``` +Clean up temp files: `rm -f graphify-out/.graphify_cached.json graphify-out/.graphify_uncached.txt graphify-out/.graphify_semantic_new.json` + +#### Part C - Merge AST + semantic into final extraction + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from pathlib import Path + +ast = json.loads(Path('graphify-out/.graphify_ast.json').read_text(encoding=\"utf-8\")) +sem = json.loads(Path('graphify-out/.graphify_semantic.json').read_text(encoding=\"utf-8\")) + +# Merge: AST nodes first, semantic nodes deduplicated by id +seen = {n['id'] for n in ast['nodes']} +merged_nodes = list(ast['nodes']) +for n in sem['nodes']: + if n['id'] not in seen: + merged_nodes.append(n) + seen.add(n['id']) + +merged_edges = ast['edges'] + sem['edges'] +merged_hyperedges = sem.get('hyperedges', []) +merged = { + 'nodes': merged_nodes, + 'edges': merged_edges, + 'hyperedges': merged_hyperedges, + 'input_tokens': sem.get('input_tokens', 0), + 'output_tokens': sem.get('output_tokens', 0), +} +Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\") +total = len(merged_nodes) +edges = len(merged_edges) +print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)') +" +``` + +### Step 4 - Build graph, cluster, analyze, generate outputs + +**Before starting:** note whether `--directed` was given. If so, pass `directed=True` to `build_from_json()` in the code block below. This builds a `DiGraph` that preserves edge direction (source→target) instead of the default undirected `Graph`. + +```bash +mkdir -p graphify-out +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.cluster import cluster, score_all +from graphify.analyze import god_nodes, surprising_connections, suggest_questions +from graphify.report import generate +from graphify.export import to_json +from pathlib import Path + +extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) +detection = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\")) + +G = build_from_json(extraction) +communities = cluster(G) +cohesion = score_all(G, communities) +tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)} +gods = god_nodes(G) +surprises = surprising_connections(G, communities) +labels = {cid: 'Community ' + str(cid) for cid in communities} +# Placeholder questions - regenerated with real labels in Step 5 +questions = suggest_questions(G, communities, labels) + +report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions) +Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") +to_json(G, communities, 'graphify-out/graph.json') + +analysis = { + 'communities': {str(k): v for k, v in communities.items()}, + 'cohesion': {str(k): v for k, v in cohesion.items()}, + 'gods': gods, + 'surprises': surprises, + 'questions': questions, +} +Path('graphify-out/.graphify_analysis.json').write_text(json.dumps(analysis, indent=2, ensure_ascii=False), encoding=\"utf-8\") +if G.number_of_nodes() == 0: + print('ERROR: Graph is empty - extraction produced no nodes.') + print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.') + raise SystemExit(1) +print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities') +" +``` + +If this step prints `ERROR: Graph is empty`, stop and tell the user what happened - do not proceed to labeling or visualization. + +Replace INPUT_PATH with the actual path. + +### Step 5 - Label communities + +Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading"). + +Then regenerate the report and save the labels for the visualizer: + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.build import build_from_json +from graphify.cluster import score_all +from graphify.analyze import god_nodes, surprising_connections, suggest_questions +from graphify.report import generate +from pathlib import Path + +extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) +detection = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\")) +analysis = json.loads(Path('graphify-out/.graphify_analysis.json').read_text(encoding=\"utf-8\")) + +G = build_from_json(extraction) +communities = {int(k): v for k, v in analysis['communities'].items()} +cohesion = {int(k): v for k, v in analysis['cohesion'].items()} +tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)} + +# LABELS - replace these with the names you chose above +labels = LABELS_DICT + +# Regenerate questions with real community labels (labels affect question phrasing) +questions = suggest_questions(G, communities, labels) + +report = generate(G, communities, cohesion, labels, analysis['gods'], analysis['surprises'], detection, tokens, 'INPUT_PATH', suggested_questions=questions) +Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\") +Path('graphify-out/.graphify_labels.json').write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding=\"utf-8\") +print('Report updated with community labels') +" +``` + +Replace `LABELS_DICT` with the actual dict you constructed (e.g. `{0: "Attention Mechanism", 1: "Training Pipeline"}`). +Replace INPUT_PATH with the actual path. + +### Step 6 - Generate Obsidian vault (opt-in) + HTML + +**Generate HTML always** (unless `--no-viz`). **Obsidian vault only if `--obsidian` was explicitly given** — skip it otherwise, it generates one file per node. + +If `--obsidian` was given: + +- If `--obsidian-dir ` was also given, pass it via `--dir`. Otherwise defaults to `graphify-out/obsidian`. + +```bash +graphify export obsidian +# or with custom dir: graphify export obsidian --dir ~/vaults/my-project +``` + +Generate the HTML graph (always, unless `--no-viz`): + +```bash +graphify export html # auto-aggregates to community view if graph > 5000 nodes +# or: graphify export html --no-viz +``` + +### Step 6b - Wiki (only if --wiki flag) + +**Only run this step if `--wiki` was explicitly given in the original command.** + +Run this before Step 9 (cleanup) so `.graphify_labels.json` is still available. + +```bash +graphify export wiki +``` + +### Step 7 - Neo4j export (only if --neo4j or --neo4j-push flag) + +**If `--neo4j`** - generate a Cypher file for manual import: + +```bash +graphify export neo4j +``` + +**If `--neo4j-push `** - push directly to a running Neo4j instance. Ask the user for credentials if not provided: + +```bash +graphify export neo4j --push bolt://localhost:7687 --user neo4j --password PASSWORD +``` + +Default URI is `bolt://localhost:7687`, default user is `neo4j`. Uses MERGE - safe to re-run without creating duplicates. + +### Step 7b - SVG export (only if --svg flag) + +```bash +graphify export svg +``` + +### Step 7c - GraphML export (only if --graphml flag) + +```bash +graphify export graphml +``` + +### Step 7d - MCP server (only if --mcp flag) + +```bash +python3 -m graphify.serve graphify-out/graph.json +``` + +This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live. + +To configure in Claude Desktop, add to `claude_desktop_config.json`: +```json +{ + "mcpServers": { + "graphify": { + "command": "python3", + "args": ["-m", "graphify.serve", "/absolute/path/to/graphify-out/graph.json"] + } + } +} +``` + +### Step 8 - Token reduction benchmark (only if total_words > 5000) + +If `total_words` from `graphify-out/.graphify_detect.json` is greater than 5,000, run: + +```bash +graphify benchmark +``` + +Print the output directly in chat. If `total_words <= 5000`, skip silently - the graph value is structural clarity, not token compression, for small corpora. + +--- + +### Step 9 - Save manifest, update cost tracker, clean up, and report + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +from datetime import datetime, timezone +from graphify.detect import save_manifest + +# Save manifest for --update +detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\")) +# In --update mode, 'all_files' carries the full corpus; 'files' is the changed +# subset. Full-rebuild mode populates only 'files', so the fallback handles that. +save_manifest(detect.get('all_files') or detect['files']) + +# Update cumulative cost tracker +extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) +input_tok = extract.get('input_tokens', 0) +output_tok = extract.get('output_tokens', 0) + +cost_path = Path('graphify-out/cost.json') +if cost_path.exists(): + cost = json.loads(cost_path.read_text(encoding=\"utf-8\")) +else: + cost = {'runs': [], 'total_input_tokens': 0, 'total_output_tokens': 0} + +cost['runs'].append({ + 'date': datetime.now(timezone.utc).isoformat(), + 'input_tokens': input_tok, + 'output_tokens': output_tok, + 'files': detect.get('total_files', 0), +}) +cost['total_input_tokens'] += input_tok +cost['total_output_tokens'] += output_tok +cost_path.write_text(json.dumps(cost, indent=2, ensure_ascii=False), encoding=\"utf-8\") + +print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens') +print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)') +" +rm -f graphify-out/.graphify_detect.json graphify-out/.graphify_extract.json graphify-out/.graphify_ast.json graphify-out/.graphify_semantic.json graphify-out/.graphify_analysis.json graphify-out/.graphify_chunk_*.json +rm -f graphify-out/.needs_update 2>/dev/null || true +``` + +Tell the user (omit the obsidian line unless --obsidian was given): +``` +Graph complete. Outputs in PATH_TO_DIR/graphify-out/ + + graph.html - interactive graph, open in browser + GRAPH_REPORT.md - audit report + graph.json - raw graph data + obsidian/ - Obsidian vault (only if --obsidian was given) +``` + +If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi + +Replace PATH_TO_DIR with the actual absolute path of the directory that was processed. + +Then paste these sections from GRAPH_REPORT.md directly into the chat: +- God Nodes +- Surprising Connections +- Suggested Questions + +Do NOT paste the full report - just those three sections. Keep it concise. + +Then immediately offer to explore. Pick the single most interesting suggested question from the report - the one that crosses the most community boundaries or has the most surprising bridge node - and ask: + +> "The most interesting question this graph can answer: **[question]**. Want me to trace it?" + +If the user says yes, run `/graphify query "[question]"` on the graph and walk them through the answer using the graph structure - which nodes connect, which community boundaries get crossed, what the path reveals. Keep going as long as they want to explore. Each answer should end with a natural follow-up ("this connects to X - want to go deeper?") so the session feels like navigation, not a one-shot report. + +The graph is the map. Your job after the pipeline is to be the guide. + +--- + +## Interpreter guard for subcommands + +Before running any subcommand below (`--update`, `--cluster-only`, `query`, `path`, `explain`, `add`), check that `.graphify_python` exists. If it's missing (e.g. user deleted `graphify-out/`), re-resolve the interpreter first: + +```bash +if [ ! -f graphify-out/.graphify_python ]; then + GRAPHIFY_BIN=$(which graphify 2>/dev/null) + if [ -n "$GRAPHIFY_BIN" ]; then + PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + case "$PYTHON" in *[!a-zA-Z0-9/_.-]*) PYTHON="python3" ;; esac + else + PYTHON="python3" + fi + mkdir -p graphify-out + "$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)" +fi +``` + +## For --update (incremental re-extraction) + +Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys, json +from graphify.detect import detect_incremental, save_manifest +from pathlib import Path + +result = detect_incremental(Path('INPUT_PATH')) +new_total = result.get('new_total', 0) +print(json.dumps(result, indent=2, ensure_ascii=False)) +Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\") +if new_total == 0: + print('No files changed since last run. Nothing to update.') + raise SystemExit(0) +print(f'{new_total} new/changed file(s) to re-extract.') +" +``` + +Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) +Path('graphify-out/.graphify_detect.json').write_text(json.dumps({ + 'files': r.get('new_files', {}), + 'all_files': r.get('files', {}), + 'total_files': r.get('new_total', 0), + 'total_words': r.get('total_words', 0), + 'skipped_sensitive': r.get('skipped_sensitive', []), + 'needs_graph': True, +}, ensure_ascii=False), encoding=\"utf-8\") +" +``` + +If new files exist, first check whether all changed files are code files: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path + +result = json.loads(open('graphify-out/.graphify_incremental.json', encoding='utf-8').read()) if Path('graphify-out/.graphify_incremental.json').exists() else {} +code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts','.lua','.toc','.f','.F','.f90','.F90','.f95','.F95','.f03','.F03','.f08','.F08'} +new_files = result.get('new_files', {}) +all_changed = [f for files in new_files.values() for f in files] +code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed) +print('code_only:', code_only) +" +``` + +If `code_only` is True: print `[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)`, run only Step 3A (AST) on the changed files, skip Step 3B entirely (no subagents), then go straight to merge and Steps 4–8. + +If `code_only` is False (any changed file is a doc/paper/image): run the full Steps 3A–3C pipeline as normal. + +Then: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from pathlib import Path +from graphify.build import build_merge +from graphify.detect import save_manifest + +# Load new extraction and incremental state +new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) +incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\")) +deleted = list(incremental.get('deleted_files', [])) + +# Use build_merge() — reads graph.json directly without NetworkX round-trip +# so edge direction (calls, implements, imports) is always preserved (#801). +G = build_merge( + [new_extraction], + graph_path='graphify-out/graph.json', + prune_sources=deleted or None, +) +print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges') + +# Write merged result back to .graphify_extract.json so Step 4 sees the full graph +merged_out = { + 'nodes': [{'id': n, **d} for n, d in G.nodes(data=True)], + 'edges': [ + # Explicit source/target last so they win over any stale attrs in d. + {**{k: val for k, val in d.items() if k not in ('_src', '_tgt', 'source', 'target')}, + 'source': d.get('_src', u), 'target': d.get('_tgt', v)} + for u, v, d in G.edges(data=True) + ], + # G.graph["hyperedges"] holds hyperedges from both existing graph.json + # and new_extraction (build_merge combines them). Falling back to + # new_extraction only would silently drop prior-run hyperedges (#801). + 'hyperedges': list(G.graph.get('hyperedges', [])), + 'input_tokens': new_extraction.get('input_tokens', 0), + 'output_tokens': new_extraction.get('output_tokens', 0), +} +Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged_out, ensure_ascii=False), encoding=\"utf-8\") +print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"])} nodes, {len(merged_out[\"edges\"])} edges)') + +# Save manifest so next --update diffs against today's state, not the +# prior run's baseline (prevents ghost-node reports on subsequent updates). +save_manifest(incremental['files']) +print('[graphify update] Manifest saved.') +" +``` + +Then run Steps 4–8 on the merged graph as normal. + +After Step 4, show the graph diff: + +```bash +$(cat graphify-out/.graphify_python) -c " +import json +from graphify.analyze import graph_diff +from graphify.build import build_from_json +from networkx.readwrite import json_graph +import networkx as nx +from pathlib import Path + +# Load old graph (before update) from backup written before merge +old_data = json.loads(Path('graphify-out/.graphify_old.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_old.json').exists() else None +new_extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\")) +G_new = build_from_json(new_extract) + +if old_data: + G_old = json_graph.node_link_graph(old_data, edges='links') + diff = graph_diff(G_old, G_new) + print(diff['summary']) + if diff['new_nodes']: + print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5])) + if diff['new_edges']: + print('New edges:', len(diff['new_edges'])) +" +``` + +Before the merge step, save the old graph: `cp graphify-out/graph.json graphify-out/.graphify_old.json` +Clean up after: `rm -f graphify-out/.graphify_old.json` + +--- + +## For --cluster-only + +Skip Steps 1–3. Re-run clustering on the existing graph: + +```bash +graphify cluster-only . +``` + +Then run Steps 5–9 as normal (label communities, generate viz, benchmark, clean up, report). + +--- + +## For /graphify query + +Two traversal modes - choose based on the question: + +| Mode | Flag | Best for | +|------|------|----------| +| BFS (default) | _(none)_ | "What is X connected to?" - broad context, nearest neighbors first | +| DFS | `--dfs` | "How does X reach Y?" - trace a specific chain or dependency path | + +```bash +graphify query "QUESTION" +# or: graphify query "QUESTION" --dfs --budget 3000 +``` + +Replace `QUESTION` with the user's actual question. Answer using **only** what the graph output contains. Quote `source_location` when citing a specific fact. If the graph lacks enough information, say so - do not hallucinate edges. + +After writing the answer, save it back into the graph so it improves future queries: + +```bash +$(cat graphify-out/.graphify_python) -m graphify save-result --question "QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2 +``` + +Replace `QUESTION` with the question, `ANSWER` with your full answer text, `SOURCE_NODES` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph. + +--- + +## For /graphify path + +Find the shortest path between two named concepts in the graph. + +```bash +graphify path "NODE_A" "NODE_B" +``` + +Replace `NODE_A` and `NODE_B` with the actual concept names. Then explain the path in plain language - what each hop means, why it's significant. + +After writing the explanation, save it back: + +```bash +$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B +``` + +--- + +## For /graphify explain + +Give a plain-language explanation of a single node - everything connected to it. + +```bash +graphify explain "NODE_NAME" +``` + +Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations. + +After writing the explanation, save it back: + +```bash +$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME +``` + +--- + +## For /graphify add + +Fetch a URL and add it to the corpus, then update the graph. + +```bash +$(cat graphify-out/.graphify_python) -c " +import sys +from graphify.ingest import ingest +from pathlib import Path + +try: + out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR') + print(f'Saved to {out}') +except ValueError as e: + print(f'error: {e}', file=sys.stderr) + sys.exit(1) +except RuntimeError as e: + print(f'error: {e}', file=sys.stderr) + sys.exit(1) +" +``` + +Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph. + +Supported URL types (auto-detected): +- YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`) +- Twitter/X → fetched via oEmbed, saved as `.md` with tweet text and author +- arXiv → abstract + metadata saved as `.md` +- PDF → downloaded as `.pdf` +- Images (.png/.jpg/.webp) → downloaded, Claude vision extracts on next run +- Any webpage → converted to markdown via html2text + +--- + +## For --watch + +Start a background watcher that monitors a folder and auto-updates the graph when files change. + +```bash +python3 -m graphify.watch INPUT_PATH --debounce 3 +``` + +Replace INPUT_PATH with the folder to watch. Behavior depends on what changed: + +- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically. +- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required). + +Debounce (default 3s): waits until file activity stops before triggering, so a wave of parallel agent writes doesn't trigger a rebuild per file. + +Press Ctrl+C to stop. + +For agentic workflows: run `--watch` in a background terminal. Code changes from agent waves are picked up automatically between waves. If agents are also writing docs or notes, you'll need a manual `/graphify --update` after those waves. + +--- + +## For git commit hook + +Install a post-commit hook that auto-rebuilds the graph after every commit. No background process needed - triggers once per commit, works with any editor. + +```bash +graphify hook install # install +graphify hook uninstall # remove +graphify hook status # check +``` + +After every `git commit`, the hook detects which code files changed (via `git diff HEAD~1`), re-runs AST extraction on those files, and rebuilds `graph.json` and `GRAPH_REPORT.md`. Doc/image changes are ignored by the hook - run `/graphify --update` manually for those. + +If a post-commit hook already exists, graphify appends to it rather than replacing it. + +--- + +## For native CLAUDE.md integration + +Run once per project to make graphify always-on in Claude Code sessions: + +```bash +graphify claude install +``` + +This writes a `## graphify` section to the local `CLAUDE.md` that instructs Claude to check the graph before answering codebase questions and rebuild it after code changes. No manual `/graphify` needed in future sessions. + +```bash +graphify claude uninstall # remove the section +``` + +--- + +## Honesty Rules + +- Never invent an edge. If unsure, use AMBIGUOUS. +- Never skip the corpus check warning. +- Always show token cost in the report. +- Never hide cohesion scores behind symbols - show the raw number. +- Never run HTML viz on a graph with more than 5,000 nodes without warning the user. diff --git a/Claude_Memory/claude_skills/grill-me/SKILL.md b/Claude_Memory/claude_skills/grill-me/SKILL.md new file mode 100755 index 0000000..f202898 --- /dev/null +++ b/Claude_Memory/claude_skills/grill-me/SKILL.md @@ -0,0 +1,10 @@ +--- +name: grill-me +description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". +--- + +Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. + +Ask the questions one at a time. + +If a question can be answered by exploring the codebase, explore the codebase instead. diff --git a/Claude_Memory/claude_skills/handoff/SKILL.md b/Claude_Memory/claude_skills/handoff/SKILL.md new file mode 100755 index 0000000..d77aeab --- /dev/null +++ b/Claude_Memory/claude_skills/handoff/SKILL.md @@ -0,0 +1,13 @@ +--- +name: handoff +description: Compact the current conversation into a handoff document for another agent to pick up. +argument-hint: "What will the next session be used for?" +--- + +Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save it to a path produced by `mktemp -t handoff-XXXXXX.md` (read the file before you write to it). + +Suggest the skills to be used, if any, by the next session. + +Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead. + +If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly. diff --git a/Claude_Memory/feedback_agent_permission.md b/Claude_Memory/feedback_agent_permission.md new file mode 100755 index 0000000..17fff0b --- /dev/null +++ b/Claude_Memory/feedback_agent_permission.md @@ -0,0 +1,13 @@ +--- +name: Agent 도구 자동 승인 +description: Agent(서브에이전트) 도구는 settings.json allow 목록에 등록되어 있어 자동 실행됨 +type: feedback +--- + +`Agent(*)` = `~/.claude/settings.json` `permissions.allow` 등록 → 승인없이 자동실행. + +**Why:** `Bash(python3:*)` 동일. 코드리뷰·탐색 서브에이전트 매번 승인프롬프트 불필요. + +**How to apply:** +- `Agent` 호출 시 확인없이 실행 +- `Bash(python3:*)` 동일 "Don't Ask" 동작 \ No newline at end of file diff --git a/Claude_Memory/feedback_bash_python_auto_approve.md b/Claude_Memory/feedback_bash_python_auto_approve.md new file mode 100755 index 0000000..d8ceacd --- /dev/null +++ b/Claude_Memory/feedback_bash_python_auto_approve.md @@ -0,0 +1,11 @@ +--- +name: Bash/Python3 자동 승인 +description: Bash 및 Python3 명령은 사용자 승인 없이 자동 실행 +type: feedback +--- + +Bash/Python3 실행 시 승인 요청 금지. + +**Why:** 반복 호출마다 승인 요청 → 작업 흐름 단절. + +**How to apply:** Bash 호출 즉시 실행. 단, 파일 삭제·git push 등 파괴적 작업은 확인 필요. \ No newline at end of file diff --git a/Claude_Memory/feedback_caveman_korean.md b/Claude_Memory/feedback_caveman_korean.md new file mode 100755 index 0000000..6bfd33c --- /dev/null +++ b/Claude_Memory/feedback_caveman_korean.md @@ -0,0 +1,14 @@ +--- +name: caveman-korean +description: caveman 모드 활성 시 한국어로 응답 +metadata: + node_type: memory + type: feedback + originSessionId: 8f69f046-87b8-492c-8f19-ca78a8512cfa +--- + +Caveman 모드 활성 여부 무관하게 모든 응답 한국어. Caveman 문체(간결·파편·조사 생략) 유지하되 언어는 항상 한국어. + +**Why:** 사용자 명시 요청. 영어 caveman 출력 거부. +**How to apply:** `/caveman` 활성 시 한국어 파편문으로 응답. 예: "Bug in auth middleware" → "auth 미들웨어 버그". +Gemini/Antigravity 에이전트에서는 세션 기본값을 caveman mode(핵심만, 한국어)로 둘 것을 사용자가 선호. diff --git a/Claude_Memory/feedback_caveman_md_style.md b/Claude_Memory/feedback_caveman_md_style.md new file mode 100755 index 0000000..733c769 --- /dev/null +++ b/Claude_Memory/feedback_caveman_md_style.md @@ -0,0 +1,14 @@ +--- +name: feedback-caveman-md-style +description: "프로젝트 기획/스펙 MD 파일은 caveman style(파편문, 간결체)로 작성" +metadata: + node_type: memory + type: feedback + originSessionId: 9eaa0ad7-0128-4d11-be9b-fa11a0bb5936 +--- + +프로젝트 기획/스펙 문서(PROJECT.md, REQUIREMENTS.md, ROADMAP.md 류 — 특정 기획 도구 무관)는 caveman style로 작성. + +**Why:** 불필요한 설명 없이 핵심 정보만. 가독성·유지보수 효율. + +**How to apply:** 문장 대신 키워드/명사구 중심. 동사는 최소화. 설명 문단 지양. diff --git a/Claude_Memory/feedback_handoff_load.md b/Claude_Memory/feedback_handoff_load.md new file mode 100755 index 0000000..dbe15c2 --- /dev/null +++ b/Claude_Memory/feedback_handoff_load.md @@ -0,0 +1,15 @@ +--- +name: "" +metadata: + node_type: memory + originSessionId: e5b7295d-190c-4567-9da4-b103fffee074 +--- + +"handoff 불러들여", "handoff 파일 로드", "이전 handoff 불러와" 등 handoff 파일 로드 요청 시: + +1. `~/.claude/last_handoff_path.txt` 읽기 → 경로 확인 +2. 해당 경로의 파일 Read → 컨텍스트 복원 + +**Why:** handoff 스킬은 매번 `/tmp/handoff-XXXXXX.md` 형태의 임시 파일을 생성하므로 경로가 고정되지 않음. PostToolUse hook이 Write 시 자동으로 `~/.claude/last_handoff_path.txt`에 경로를 저장. + +**How to apply:** handoff 로드 요청 시 파일 경로 추측 금지. 반드시 `~/.claude/last_handoff_path.txt` 먼저 읽을 것. diff --git a/Claude_Memory/feedback_linter_before_commit.md b/Claude_Memory/feedback_linter_before_commit.md new file mode 100755 index 0000000..6c51602 --- /dev/null +++ b/Claude_Memory/feedback_linter_before_commit.md @@ -0,0 +1,17 @@ +--- +name: Commit 전 Linter 수행 +description: git commit 전 변경 파일에 linter 실행, 통과 후 commit +type: feedback +--- + +`git commit` 전 변경 파일에 linter 실행. + +**Why:** 린팅 오류 커밋 이력 방지. + +**How to apply:** +- `git diff --name-only HEAD` 또는 `git status`로 변경 파일 확인. +- `.clang-format` 있으면 `clang-format --dry-run`으로 C++ 검사. +- `.clang-tidy` 있으면 `clang-tidy` 정적 분석. +- 기타 linter(eslint, pylint 등) 있으면 실행. +- linter 오류 → 수정 후 commit. +- linter 설정 없어도 사용자에게 수행 여부 확인. \ No newline at end of file diff --git a/Claude_Memory/feedback_truncated_output.md b/Claude_Memory/feedback_truncated_output.md new file mode 100755 index 0000000..88532ee --- /dev/null +++ b/Claude_Memory/feedback_truncated_output.md @@ -0,0 +1,14 @@ +--- +name: feedback-truncated-output +description: "system-reminder에 \"Output too large / Preview\" 표시 시 전체 파일 읽기 필수" +metadata: + node_type: memory + type: feedback + originSessionId: 442b5c1a-bf31-4f49-be3a-abe955ac1516 +--- + +system-reminder에 "Output too large... Full output saved to: " 메시지가 있으면, 미리보기(Preview)만 보고 내용을 판단하지 말 것. + +**Why:** 2KB 미리보기만 보고 "CHECKPOINT.md / TODO.md만 주입됐다"고 잘못 단정한 사례 발생. 실제로는 NOTES.md, TIPS.md, 로그 파일 2개까지 모두 주입됐음. + +**How to apply:** "Output too large" 문구 확인 시 → 저장된 전체 파일 경로를 Read 또는 `cat`으로 읽은 후 판단. diff --git a/Claude_Memory/feedback_worktree_check.md b/Claude_Memory/feedback_worktree_check.md new file mode 100755 index 0000000..b1de03b --- /dev/null +++ b/Claude_Memory/feedback_worktree_check.md @@ -0,0 +1,16 @@ +--- +name: 코드 작업 전 Worktree 확인 +description: 코드 작업 전 `git worktree list` + `.worktrees/` 확인 → 기존 worktree 재사용 or 신규 생성 여부 사용자 확인 +type: feedback +--- + +코드 작업 전 반드시 worktree 상태 확인. + +**Why:** 기존 worktree 모르고 main 브랜치 작업 or 브랜치 중복 생성 방지. + +**How to apply:** +1. `git worktree list`로 등록된 worktree 목록 확인. +2. `.worktrees/` 하위 폴더명 확인 (`ls .worktrees/` 또는 `Glob`). +3. 관련 worktree 존재 → 해당 worktree 사용. +4. 관련 worktree 없음 → 사용자에게 신규 생성 여부 확인. +5. 생성 시 `.worktrees/<작업명>` 경로 + 브랜치명 사용자 협의. \ No newline at end of file diff --git a/Claude_Memory/reference_graphify.md b/Claude_Memory/reference_graphify.md new file mode 100755 index 0000000..4008df0 --- /dev/null +++ b/Claude_Memory/reference_graphify.md @@ -0,0 +1,88 @@ +--- +name: reference_graphify +description: graphify 도구 전체 개요 — 설치·실행 방법, 출력물 위치, 쿼리 방법 +metadata: + type: reference +--- + +## 개요 + +`graphify` (pip 패키지명: `graphifyy`)는 폴더의 코드·문서·이미지·영상을 Knowledge Graph로 변환하는 도구. +트리거: `/graphify` 슬래시 명령 → Skill tool `skill: "graphify"` 호출 필수. + +> **공유 리소스 가능성**: graphify 출력물을 다른 AI Agent(예: Google Antigravity)와 공유하는 경우, 출력물 수정·재빌드 시 다른 Agent의 참조에 영향을 줄 수 있으므로 주의. + +- Skill 위치: `Claude_Memory/claude_skills/graphify/SKILL.md` + +--- + +## 출력물 구조 (`graphify-out/`) + +| 파일 | 설명 | +|------|------| +| `GRAPH_REPORT.md` | 코드베이스 질문 시 **먼저 읽어야 할 파일**. God Nodes·커뮤니티·Surprising Connections 포함 | +| `graph.json` | 원본 그래프 데이터 (GraphRAG-ready JSON) | +| `graph.html` | 브라우저에서 여는 인터랙티브 시각화 | +| `manifest.json` | 이전 실행 기록 (incremental update용) | +| `cache/` | 파일별 시멘틱 추출 캐시 | +| `.graphify_labels.json` | 커뮤니티 레이블 | +| `.graphify_python` | 사용 중인 Python 인터프리터 경로 | +| `.graphify_root` | 스캔 루트 경로 | + +--- + +## 주요 명령 + +```bash +# 최초 빌드 +/graphify + +# 변경 파일만 재추출 (코드만 변경 시 LLM 불필요) +/graphify --update + +# 그래프 질문 (BFS: 넓은 컨텍스트) +/graphify query "<질문>" + +# 그래프 질문 (DFS: 특정 경로 추적) +/graphify query "<질문>" --dfs + +# 두 개념 간 최단 경로 +/graphify path "" "" + +# 단일 노드 설명 +/graphify explain "<노드명>" + +# 커뮤니티 재클러스터링만 (추출 생략) +/graphify --cluster-only +``` + +--- + +## 관리 정책 + +- graphify 출력물(`graphify-out/`)은 대상 프로젝트 루트가 아니라 **이 메모리 폴더(`Agent_Memories`) 하위**에서 관리하는 것을 기본 원칙으로 한다 (프로젝트 저장소를 캐시로 오염시키지 않기 위함). +- 프로젝트별로 스캔 제외 규칙(불필요한 대용량/생성물 폴더)이 필요할 수 있음 — 대상 프로젝트의 `.gitignore` 패턴을 기본으로 따르고, 프로젝트 전용 제외 규칙은 해당 프로젝트의 자체 메모리/문서에 기록한다. + +--- + +## 사용 규칙 (CLAUDE.md에서) + +1. **소스 파일 읽기·grep/glob 검색·코드베이스 질문 전 항상 `graphify-out/GRAPH_REPORT.md` 먼저 읽기** +2. `graphify-out/wiki/index.md` 존재 시 raw 파일 대신 탐색 +3. 교차 모듈 관계 질문: grep 대신 `graphify query/path/explain` 우선 +4. graphify 출력물은 대상 프로젝트 루트가 아닌 이 메모리 폴더 하위에서 관리 + +--- + +## 설치 확인 + +```bash +# uv 기반 (권장) +uv tool install --upgrade graphifyy + +# pip 기반 +pip install graphifyy --break-system-packages +``` + +GEMINI_API_KEY 또는 GOOGLE_API_KEY 미설정 시 → Claude Code 서브에이전트로 시멘틱 추출 수행. +설정 시 → Gemini(`gemini-3-flash-preview`) 사용으로 토큰 절감. diff --git a/Claude_Memory/user_code_comment_style.md b/Claude_Memory/user_code_comment_style.md new file mode 100755 index 0000000..ecffa88 --- /dev/null +++ b/Claude_Memory/user_code_comment_style.md @@ -0,0 +1,16 @@ +--- +name: 코드 주석 스타일 규칙 +description: 구현 시 주요 Logic→영문 주석, 필요시 Doxygen +type: user +--- + +Impl: EN comment per major logic block. Doxygen for func/class/complex logic. + +**Applies to:** 모든 코드베이스 (프로젝트 무관, 2026-04-08 global 규칙으로 격상됨) + +**Why:** readability/doc quality 선호, all codebases. + +**How to apply:** +- Every significant logic block → short English comment describing intent +- Function/class definitions → Doxygen `/** @brief ... @param ... @return ... */` +- Complex conditionals → inline comment: condition purpose \ No newline at end of file diff --git a/Claude_Memory/user_developer_profile.md b/Claude_Memory/user_developer_profile.md new file mode 100755 index 0000000..3ffe247 --- /dev/null +++ b/Claude_Memory/user_developer_profile.md @@ -0,0 +1,27 @@ +--- +name: user_developer_profile +description: 사용자의 개발자 성향 프로필 — 소통/의사결정/디버깅/UX 철학 등 +metadata: + type: user +--- + +| Dimension | Rating | Confidence | +|-----------|--------|------------| +| Communication | terse-direct | HIGH | +| Decisions | fast-intuitive | HIGH | +| Explanations | concise | MEDIUM | +| Debugging | diagnostic | MEDIUM | +| UX Philosophy | function-first | MEDIUM | +| Vendor Choices | opinionated | LOW | +| Frustrations | instruction-adherence | MEDIUM | +| Learning | self-directed | MEDIUM | + +**Directives:** +- **Communication:** 간결·행동. 결과 먼저. 군더더기 없음. +- **Decisions:** 명확 추천, 빠르게. 목록 금지(요청 시 제외). +- **Explanations:** 설명+코드 간결. 한 문장 이유. 설명 불필요(요청 시 제외). +- **Debugging:** 원인 먼저, 수정. +- **UX Philosophy:** 기능 우선. 스타일 금지(요청 시). UX=사용성/데이터흐름. +- **Vendor Choices:** 기술 제약 준수. 대체 라이브러리 금지(요청 시). 최소 의존성. +- **Frustrations:** 지시 정확히. 반복 재독. 범위 확장 금지. 모호 시 질문. +- **Learning:** 맥락 파악 전제. 직접 답변. 기본 생략. diff --git a/Claude_Memory/user_session_start_skills.md b/Claude_Memory/user_session_start_skills.md new file mode 100755 index 0000000..54a8a87 --- /dev/null +++ b/Claude_Memory/user_session_start_skills.md @@ -0,0 +1,11 @@ +--- +name: 세션 시작 시 활성 스킬 안내 +description: 세션 시작 시 활성 skill 목록 사용자에게 알릴 것 +type: user +--- + +세션 시작 시 활성 Claude skill 목록 사용자에게 안내. + +**Why:** 사용자가 `/skills` 없이 활성 스킬 파악 가능. + +**How to apply:** 새 세션 시작 시 (context 요약 후 첫 응답 포함) system-reminder 의 skills 목록 확인, 표 형식으로 제시. \ No newline at end of file diff --git a/Claude_Memory/user_usage_alarm.md b/Claude_Memory/user_usage_alarm.md new file mode 100755 index 0000000..3e8ca0f --- /dev/null +++ b/Claude_Memory/user_usage_alarm.md @@ -0,0 +1,16 @@ +--- +name: user-usage-alarm +description: Claude Code usage 리셋 알람 패턴 — 사용자가 usage 꽉 찼다고 하면 1시간 후 PushNotification 예약 +metadata: + node_type: memory + type: feedback + originSessionId: 246c0ef0-1a76-42ef-bcc3-483d58b9fc21 +--- + +사용자 "usage 꽉 찼다"/"usage full" 전달 시: +1. 즉시 `ScheduleWakeup(delaySeconds=18000)` 호출 — 5시간 후 예약 +2. 5시간 후 `PushNotification` 발송 — "Claude Code usage가 리셋됐을 것 같습니다" + +**Why:** Claude Code usage % 조회 API 없음 — 직접 감지 불가. 사용자 수동 알림 기준 5시간 타이머. + +**How to apply:** 사용자 usage 한도 도달 메시지 수신 즉시 — 확인 없이 ScheduleWakeup 실행. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..bd203d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 JunHo Kim + +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. diff --git a/README.md b/README.md new file mode 100755 index 0000000..5ea3a5e --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Agent Memories + +A portable, project-agnostic memory and skill scaffold for AI coding agents (Claude Code, Gemini/Antigravity, and similar). + +The core idea: an agent that works across many different codebases accumulates two kinds of knowledge — +knowledge that is useful *everywhere* (how you like to communicate, how you like commits done, reusable +tool workflows) and knowledge that only makes sense for *one specific project* (its language, its build +system, its domain quirks). Mixing the two makes the "everywhere" knowledge unusable when you switch to a +different project. This repository keeps only the first kind. Project-specific knowledge lives inside each +project's own repository instead. + +## Structure + +``` +Agent_Memories/ +├── Claude_Memory/ # Claude-specific working memory +│ ├── MEMORY.md # index — start here +│ ├── claude_info/ # CHECKPOINT / NOTES / TIPS / TODO (session-scoped scratch files) +│ ├── claude_log/ # dated work logs (LOG_YYYYMMDD_.md), format only — see README inside +│ ├── claude_skills/ # mirrored Skill definitions (archive-session, graphify, grill-me, handoff) +│ ├── feedback_*.md # behavioral rules learned from user corrections/confirmations +│ ├── user_*.md # stable facts about the user (preferences, profile) +│ └── reference_*.md # reusable tool/reference documentation (e.g. graphify usage) +├── caveman/ # ultra-compressed communication mode (skill + subcommands) +├── html-slide-deck/ # skill for building self-contained HTML slide decks +└── html-slide-deck.skill # packaged version of the above +``` + +### Memory file conventions (`Claude_Memory/`) + +| Prefix | Meaning | Changes how often? | +|---|---|---| +| `feedback_*.md` | A behavioral rule learned from an explicit user correction or confirmation. Each file states the rule, **why** it exists, and **how** to apply it. | Rare — only when the user corrects or confirms an approach | +| `user_*.md` | Stable facts about the user: preferences, working style, profile. | Rare | +| `reference_*.md` | Documentation for a reusable tool or workflow that isn't tied to any one project. | Occasional | +| `claude_info/*.md` | Short-lived session scratch space (current status, discoveries, todos). | Every session | +| `claude_log/*.md` | Dated logs of non-obvious discoveries, kept for future reference. | Per notable discovery | + +**Rule of thumb**: if a note only makes sense to someone who already knows the specific project you were +working in, it does not belong here — move it into that project's own memory/notes instead. This repository +was extracted from a working setup that originally mixed both kinds of content; project-specific material +was moved out during that cleanup. + +## Usage + +1. Clone or copy this repository's contents into wherever your agent looks for pers \ No newline at end of file diff --git a/caveman/cavecrew/README.md b/caveman/cavecrew/README.md new file mode 100755 index 0000000..e722952 --- /dev/null +++ b/caveman/cavecrew/README.md @@ -0,0 +1,41 @@ +# cavecrew + +Decision guide. When to delegate to caveman subagents instead of doing the work inline. + +## What it does + +Tells the main thread when to spawn a caveman-style subagent versus the vanilla equivalent. The win: subagent tool-results inject back into main context verbatim, and caveman output is roughly 1/3 the size of vanilla prose. Across 20 delegations in one session, that is the difference between context exhaustion and finishing the task. + +Three subagents: + +| Subagent | Job | Use when | +|----------|-----|----------| +| `cavecrew-investigator` | Locate code (read-only) | "Where is X defined / what calls Y / list uses of Z" | +| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. | +| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji | + +Use vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors. + +This skill is a decision guide, not a slash command. It activates when the conversation mentions delegation. + +## How to invoke + +Triggers on phrases like "delegate to subagent", "use cavecrew", "spawn investigator", "save context", "compressed agent output". + +## Example chaining + +Locate → fix → verify (most common): + +1. `cavecrew-investigator` returns site list (`path:line — symbol — note`) +2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder` +3. `cavecrew-reviewer` audits the resulting diff + +Parallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main. + +## See also + +- [`SKILL.md`](./SKILL.md) — full decision matrix and output contracts +- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md) +- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md) +- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md) +- [Caveman README](../../README.md) — repo overview diff --git a/caveman/cavecrew/SKILL.md b/caveman/cavecrew/SKILL.md new file mode 100755 index 0000000..efa413f --- /dev/null +++ b/caveman/cavecrew/SKILL.md @@ -0,0 +1,82 @@ +--- +name: cavecrew +description: > + Decision guide for delegating to caveman-style subagents. Tells the main + thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder` + (1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the + work inline or using vanilla `Explore`. Subagent output is caveman-compressed + so the tool-result injected back into main context is ~60% smaller — main + context lasts longer across long sessions. + Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer", + "save context", "compressed agent output". +--- + +Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation. + +## When to use cavecrew vs alternatives + +| Task | Use | +|---|---| +| "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` | +| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) | +| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` | +| New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` | +| Review diff, branch, or file for bugs | `cavecrew-reviewer` | +| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) | +| One-line answer you already know | Main thread, no subagent | + +Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.** + +## Why this exists (the real win) + +Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task. + +## Output contracts + +What main thread can rely on per agent: + +**`cavecrew-investigator`** +``` +
: +- path:line — `symbol` — short note +totals: . +``` +Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`. + +**`cavecrew-builder`** +``` + — . +verified: . +``` +Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token). + +**`cavecrew-reviewer`** +``` +path:line: : . . +totals: N🔴 N🟡 N🔵 N❓ +``` +Or `No issues.` Findings sorted file → line ascending. + +## Chaining patterns + +**Locate → fix → verify** (most common): +1. `cavecrew-investigator` returns site list. +2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`. +3. `cavecrew-reviewer` audits the diff. + +**Parallel scout** (when investigation is broad): +Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread. + +**Single-shot edit** (when site is already known): +Skip investigator. Hand exact path:line to `cavecrew-builder` directly. + +## What NOT to do + +- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context. +- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn. +- Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that. +- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase. + +## Auto-clarity (inherited) + +Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after. diff --git a/caveman/caveman-commit/README.md b/caveman/caveman-commit/README.md new file mode 100755 index 0000000..d5aee01 --- /dev/null +++ b/caveman/caveman-commit/README.md @@ -0,0 +1,44 @@ +# caveman-commit + +Terse Conventional Commits. Why over what. + +## What it does + +Generates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context. + +Outputs only the message. Does not stage, commit, or amend. + +## How to invoke + +``` +/caveman-commit +``` + +Also triggers on phrases like "write a commit", "commit message", "generate commit". + +## Example output + +Diff: new endpoint for user profile. + +``` +feat(api): add GET /users/:id/profile + +Mobile client needs profile data without the full user payload +to reduce LTE bandwidth on cold-launch screens. + +Closes #128 +``` + +Diff: breaking API rename. + +``` +feat(api)!: rename /v1/orders to /v1/checkout + +BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout +before 2026-06-01. Old route returns 410 after that date. +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview diff --git a/caveman/caveman-commit/SKILL.md b/caveman/caveman-commit/SKILL.md new file mode 100755 index 0000000..1f90fd8 --- /dev/null +++ b/caveman/caveman-commit/SKILL.md @@ -0,0 +1,65 @@ +--- +name: caveman-commit +description: > + Ultra-compressed commit message generator. Cuts noise from commit messages while preserving + intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why" + isn't obvious. Use when user says "write a commit", "commit message", "generate commit", + "/commit", or invokes /caveman-commit. Auto-triggers when staging changes. +--- + +Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what. + +## Rules + +**Subject line:** +- `[](): ` — `` optional +- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert` +- Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding" +- ≤50 chars when possible, hard cap 72 +- No trailing period +- Match project convention for capitalization after the colon + +**Body (only if needed):** +- Skip entirely when subject is self-explanatory +- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues +- Wrap at 72 chars +- Bullets `-` not `*` +- Reference issues/PRs at end: `Closes #42`, `Refs #17` + +**What NEVER goes in:** +- "This commit does X", "I", "we", "now", "currently" — the diff says what +- "As requested by..." — use Co-authored-by trailer +- "Generated with Claude Code" or any AI attribution +- Emoji (unless project convention requires) +- Restating the file name when scope already says it + +## Examples + +Diff: new endpoint for user profile with body explaining the why +- ❌ "feat: add a new endpoint to get user profile information from the database" +- ✅ + ``` + [feat](api): add GET /users/:id/profile + + Mobile client needs profile data without the full user payload + to reduce LTE bandwidth on cold-launch screens. + + Closes #128 + ``` + +Diff: breaking API change +- ✅ + ``` + [feat](api)!: rename /v1/orders to /v1/checkout + + BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout + before 2026-06-01. Old route returns 410 after that date. + ``` + +## Auto-Clarity + +Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context. + +## Boundaries + +Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style. diff --git a/caveman/caveman-compress/README.md b/caveman/caveman-compress/README.md new file mode 100755 index 0000000..b0fe65d --- /dev/null +++ b/caveman/caveman-compress/README.md @@ -0,0 +1,163 @@ +

+ +

+ +

caveman-compress

+ +

+ shrink memory file. save token every session. +

+ +--- + +A Claude Code skill that compresses your project memory files (`CLAUDE.md`, todos, preferences) into caveman format — so every session loads fewer tokens automatically. + +Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman make file small. Cost go down forever. + +## What It Do + +``` +/caveman-compress CLAUDE.md +``` + +``` +CLAUDE.md ← compressed (Claude reads this — fewer tokens every session) +CLAUDE.original.md ← human-readable backup (you edit this) +``` + +Original never lost. You can read and edit `.original.md`. Run skill again to re-compress after edits. + +## Benchmarks + +Real results on real project files: + +| File | Original | Compressed | Saved | +|------|----------:|----------:|------:| +| `claude-md-preferences.md` | 706 | 285 | **59.6%** | +| `project-notes.md` | 1145 | 535 | **53.3%** | +| `claude-md-project.md` | 1122 | 636 | **43.3%** | +| `todo-list.md` | 627 | 388 | **38.1%** | +| `mixed-with-code.md` | 888 | 560 | **36.9%** | +| **Average** | **898** | **481** | **46%** | + +All validations passed ✅ — headings, code blocks, URLs, file paths preserved exactly. + +## Before / After + + + + + + +
+ +### 📄 Original (706 tokens) + +> "I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime." + + + +### 🪨 Caveman (285 tokens) + +> "Prefer TypeScript strict mode always. No `any` unless unavoidable — comment why if used. Proper types catch bugs early." + +
+ +**Same instructions. 60% fewer tokens. Every. Single. Session.** + +## Security + +`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do. + +## Install + +Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`. + +If you need local files, the compress skill lives at: + +```bash +caveman-compress/ +``` + +**Requires:** Python 3.10+ + +## Usage + +``` +/caveman-compress +``` + +Examples: +``` +/caveman-compress CLAUDE.md +/caveman-compress docs/preferences.md +/caveman-compress todos.md +``` + +### What files work + +| Type | Compress? | +|------|-----------| +| `.md`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | ✅ Yes | +| Extensionless natural language | ✅ Yes | +| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) | +| `*.original.md` | ❌ Skip (backup files) | + +## How It Work + +``` +/caveman-compress CLAUDE.md + ↓ +detect file type (no tokens) + ↓ +Claude compresses (tokens — one call) + ↓ +validate output (no tokens) + checks: headings, code blocks, URLs, file paths, bullets + ↓ +if errors: Claude fixes cherry-picked issues only (tokens — targeted fix) + does NOT recompress — only patches broken parts + ↓ +retry up to 2 times + ↓ +write compressed → CLAUDE.md +write original → CLAUDE.original.md +``` + +Only two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python. + +## What Is Preserved + +Caveman compress natural language. It never touch: + +- Code blocks (` ``` ` fenced or indented) +- Inline code (`` `backtick content` ``) +- URLs and links +- File paths (`/src/components/...`) +- Commands (`npm install`, `git commit`) +- Technical terms, library names, API names +- Headings (exact text preserved) +- Tables (structure preserved, cell text compressed) +- Dates, version numbers, numeric values + +## Why This Matter + +`CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs tokens every single time you open a project. Over 100 sessions that's 100,000 tokens of overhead — just for context you already wrote. + +Caveman cut that by ~46% on average. Same instructions. Same accuracy. Less waste. + +``` +┌────────────────────────────────────────────┐ +│ TOKEN SAVINGS PER FILE █████ 46% │ +│ SESSIONS THAT BENEFIT ██████████ 100% │ +│ INFORMATION PRESERVED ██████████ 100% │ +│ SETUP TIME █ 1x │ +└────────────────────────────────────────────┘ +``` + +## Part of Caveman + +This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit — making Claude use fewer tokens without losing accuracy. + +- **caveman** — make Claude *speak* like caveman (cuts response tokens ~65%) +- **caveman-compress** — make Claude *read* less (cuts context tokens ~46%) diff --git a/caveman/caveman-compress/SECURITY.md b/caveman/caveman-compress/SECURITY.md new file mode 100755 index 0000000..693108c --- /dev/null +++ b/caveman/caveman-compress/SECURITY.md @@ -0,0 +1,31 @@ +# Security + +## Snyk High Risk Rating + +`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do. + +### What triggers the rating + +1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument. + +2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved alongside it. No files outside the user-specified path are read or written. + +### What the skill does NOT do + +- Does not execute user file content as code +- Does not make network requests except to Anthropic's API (via SDK or CLI) +- Does not access files outside the path the user provides +- Does not use shell=True or string interpolation in subprocess calls +- Does not collect or transmit any data beyond the file being compressed + +### Auth behavior + +If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication. + +### File size limit + +Files larger than 500KB are rejected before any API call is made. + +### Reporting a vulnerability + +If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`. diff --git a/caveman/caveman-compress/SKILL.md b/caveman/caveman-compress/SKILL.md new file mode 100755 index 0000000..00ce454 --- /dev/null +++ b/caveman/caveman-compress/SKILL.md @@ -0,0 +1,111 @@ +--- +name: caveman-compress +description: > + Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format + to save input tokens. Preserves all technical substance, code, URLs, and structure. + Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md. + Trigger: /caveman-compress FILEPATH or "compress memory file" +--- + +# Caveman Compress + +## Purpose + +Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `.original.md`. + +## Trigger + +`/caveman-compress ` or when user asks to compress a memory file. + +## Process + +1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md. + +2. From the directory containing this SKILL.md, run: + +python3 -m scripts + +3. The CLI will: +- detect file type (no tokens) +- call Claude to compress +- validate output (no tokens) +- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression) +- retry up to 2 times +- if still failing after 2 retries: report error to user, leave original file untouched + +4. Return result to user + +## Compression Rules + +### Remove +- Articles: a, an, the +- Filler: just, really, basically, actually, simply, essentially, generally +- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend" +- Hedging: "it might be worth", "you could consider", "it would be good to" +- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because" +- Connective fluff: "however", "furthermore", "additionally", "in addition" + +### Preserve EXACTLY (never modify) +- Code blocks (fenced ``` and indented) +- Inline code (`backtick content`) +- URLs and links (full URLs, markdown links) +- File paths (`/src/components/...`, `./config.yaml`) +- Commands (`npm install`, `git commit`, `docker build`) +- Technical terms (library names, API names, protocols, algorithms) +- Proper nouns (project names, people, companies) +- Dates, version numbers, numeric values +- Environment variables (`$HOME`, `NODE_ENV`) + +### Preserve Structure +- All markdown headings (keep exact heading text, compress body below) +- Bullet point hierarchy (keep nesting level) +- Numbered lists (keep numbering) +- Tables (compress cell text, keep structure) +- Frontmatter/YAML headers in markdown files + +### Compress +- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize" +- Fragments OK: "Run tests before commit" not "You should always run tests before committing" +- Drop "you should", "make sure to", "remember to" — just state the action +- Merge redundant bullets that say the same thing differently +- Keep one example where multiple examples show the same pattern + +CRITICAL RULE: +Anything inside ``` ... ``` must be copied EXACTLY. +Do not: +- remove comments +- remove spacing +- reorder lines +- shorten commands +- simplify anything + +Inline code (`...`) must be preserved EXACTLY. +Do not modify anything inside backticks. + +If file contains code blocks: +- Treat code blocks as read-only regions +- Only compress text outside them +- Do not merge sections around code + +## Pattern + +Original: +> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production. + +Compressed: +> Run tests before push to main. Catch bugs early, prevent broken prod deploys. + +Original: +> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens. + +Compressed: +> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens. + +## Boundaries + +- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless) +- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh +- If file has mixed content (prose + code), compress ONLY the prose sections +- If unsure whether something is code or prose, leave it unchanged +- Original file is backed up as FILE.original.md before overwriting +- Never compress FILE.original.md (skip it) diff --git a/caveman/caveman-compress/scripts/__init__.py b/caveman/caveman-compress/scripts/__init__.py new file mode 100755 index 0000000..16b8c53 --- /dev/null +++ b/caveman/caveman-compress/scripts/__init__.py @@ -0,0 +1,9 @@ +"""Caveman compress scripts. + +This package provides tools to compress natural language markdown files +into caveman format to save input tokens. +""" + +__all__ = ["cli", "compress", "detect", "validate"] + +__version__ = "1.0.0" diff --git a/caveman/caveman-compress/scripts/__main__.py b/caveman/caveman-compress/scripts/__main__.py new file mode 100755 index 0000000..4e28416 --- /dev/null +++ b/caveman/caveman-compress/scripts/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/caveman/caveman-compress/scripts/__pycache__/__init__.cpython-312.pyc b/caveman/caveman-compress/scripts/__pycache__/__init__.cpython-312.pyc new file mode 100755 index 0000000000000000000000000000000000000000..fb0e59c1ecc2eb7c8ea9985f55c12ef8b785e148 GIT binary patch literal 406 zcmYLGu};G<5Vey)tF&bTX0IKHi^PavV1|jQi)F=b>&CGo+X-qX_yA^hHh!Q#!5?I* z#18=7I$=wxIO*=*S$Fr|$)BT93cTHK(&g6YuTUH;>cH;QgEzpy6wCpG?{ErPG>umA zI9b2m(T0~)pSWE zI=()-%qH37VH~$TLYU4834luJ!GMs48!NOT}6oW Gcl-ex4uL@c literal 0 HcmV?d00001 diff --git a/caveman/caveman-compress/scripts/__pycache__/__main__.cpython-312.pyc b/caveman/caveman-compress/scripts/__pycache__/__main__.cpython-312.pyc new file mode 100755 index 0000000000000000000000000000000000000000..1e614e902b85f6145f00e7299f4631c123278634 GIT binary patch literal 214 zcmX@j%ge<81ist2vgCmDV-N=hn4pZ$Aere5DGVu$ISjdsQH+cXObn@vtD(}243$ip z%r6;%TusJXEV+r9d48Hqx0sW2GK-jjf-4z5gH-;q*U!k$P1VoJ$j;2w*Go)K%_}L^ zFV4=)$tl)PPAp5!P0Z6x&d)6lIY~;;_lhPbtkwwJYKP dng_D4m={QVU}j`wyvrc^kb$L@t%waM0swf*GXnqs literal 0 HcmV?d00001 diff --git a/caveman/caveman-compress/scripts/__pycache__/cli.cpython-312.pyc b/caveman/caveman-compress/scripts/__pycache__/cli.cpython-312.pyc new file mode 100755 index 0000000000000000000000000000000000000000..ee8d28c2e1dae467ece699876b99fd005ef15ad0 GIT binary patch literal 3055 zcmbtWUu@gP89!2#M3K~AYe{WqH4`UwmB!Mtt*GaAEdnR06Q^mrU_hfG6KL^l)29C5 zcuXR}9*q3~V$`i2pg>#{MfKPs1=goxuLXv+Kwp$vT^7PXyVZTjo-}6zitJ_IQDVfn zL6>0%@Q(N0_y0crK7JaFN(jaaA4>BH5uvZx#v7sT%bmZ&%Qd8#acbWO_t2!yiW%RD|*Zk8ltRdO+u|2RaP-5YWOnbe-$$EpByB1OY9i3wrSV&~?5; z=g{f_fmsimk&={`IHZeu#FR@)UUA=L>hIFBxInOOZvX+pb*OIlf-sFbP%Tb4R;=9DD; z+|p+8xTL};b{U?TF-%<5=-ji?ZKnLJBNJS#lxK`to8WrTrZb~sPMF}TsTFaCcSKw+ zR&=90>x3{N6=G#LM|cSo+-5m|O|;mxSzxL`BB5i7i`3l~XswE!xHVU?O}*f87Q11& z3rj6~?hL?m4bg6D5v0#cGlY*a{MmYcC!hxe1AYXn#3g1r?s#slQo_0Uxdo$? z%R(B;)XG^4hG|;4ZZ1YWcXC#d7*#kgnnt!-b7WUj!DiVbNzk?r9!rL48EpxXrgZ3Y z;V}}2{e6K1f#3#`hFbFnLc3Lg(1WVb3sUD6o`QQHqG8bEj^6E4rq>1o$m)OPi_ekd7zh~7eP`)xk7cVLC(bJuvF72*G~ z(znY5Y&|PWhCm^FXW#K2yg@6osF=UbKvadRP0eBh_b73 zhtEHLkte_KNkn=Qkkvs?qPGH-)$OjKOLd@lvP%Juff>EskB#*nNs$seEWsTZIGgVV7&)NW8>f-?1IM)I93}Ke*>N%>g=^Cljp+A$av~=#7jcz>|Cyj8dTrE+u(wW|3wLwm$kSSJ z!LAlqpLIBFvChDnuu9n7IzmY^%7kS`g~xgoz#AG%SX%wj+Stn2Z&YeDsRX8-k7*Hd3AE*<*QShadmmJ9YRWa?YWibTJpXvG1^QVY`oGq-Hhd0 z;tza+U&?(q<-r^BU`zhK%ceFSX-Li36D{$GpDn)kW_;*Id}uSie|hqsA(Y(Tcw{qi zc=^<}5V>-Cy|%v4ln%6nVK`C-H^y4>L8dKsyd|FSsR8DuG};ofZJ{ri-bxKM#LZOZ zO8BpGYD-BrQ)4aVCx00@aC6|uje#TYzViDE?_K!AFK>)aZ4R8dG6fv#XI$@6PqdUH zo$X*F+Dv6zN^VPuuT8E@uD|@+)HYK!-cp|S(Z1Q1_w67)xW{$tP)mHwbF1(5=(e0@ zXK6TFk8Wri=bEwoE%AWIkzA8kh65j`L^cl$ zPx66J_#nJHVxhpUvQWtMIpIP62kD) z5sR8>YSShrLu?yn1!Ef1oe~z=$H#u=IyD;V{8sr&m$g#2jF=;=c)@y%(HL&KrFK_* zrc}{w6F*Ci0*(D8SXW_c2RM%VhY;k1J2Aw`Z3!jQ*QBe`#>6|P-#p!nkL;ijmt21S zR;qtvr18@a4sNENTAsQUj<3s~hxfJlKqPrf+1uFrLI0nXr`r+W{Z|5U{a>TuFVK@a zA&wi_5sz}|_8f-<sa4{N2fR00{%%v?q9!=-)vcQ<4BzBFVcJi{(S<*kS&o O!@*-e41RQ!gZV!`5SNbt literal 0 HcmV?d00001 diff --git a/caveman/caveman-compress/scripts/__pycache__/compress.cpython-312.pyc b/caveman/caveman-compress/scripts/__pycache__/compress.cpython-312.pyc new file mode 100755 index 0000000000000000000000000000000000000000..7be1b8b7faf34fd2e6475f3dac83929834ec8762 GIT binary patch literal 9999 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 literal 0 HcmV?d00001 diff --git a/caveman/caveman-compress/scripts/__pycache__/detect.cpython-312.pyc b/caveman/caveman-compress/scripts/__pycache__/detect.cpython-312.pyc new file mode 100755 index 0000000000000000000000000000000000000000..72a1f02a062f0ef391cd8204e2a5119e16dfbc72 GIT binary patch literal 5834 zcmb^#TWlN0agTR=lcFR_mSjctS+-)+im5nKWJi|e#GRwO5KQd^e7o_R;{D89UR zdXS+2r41UXHDIMLGi?eGXn@L1g9a$j=A&QvD$pO&RABk)0uGXo`j4^lk?5;4M;$G{ z>HzHmH@iDKJF`1GGdp|#aJwA@%FBOoL~eTs`2ssev2Qfi-Zv3)m55}Nh>U28GNX)M zn?_Agvr#t2jdBbL6OsEJGirv~47J5XBvYiyV0Nz-Egv%F-eW1OPo%Itk;48&3da*E zoKK{1859%NQJd(N>|&MV2yvq4ZEn;lR#*Bp(07To5>Pb3<~OLkfUOeiBo6v^m{kvb z&qg1)Y=M5YR3kP_?jeN!RM&d#Xf5c?E7e8-_h2zrBQ{RLnbEC4SuZvjScX8qHuB7a zvtT`2@6kM(v$$0>Z6(Lact0Vd4Zw$!8jLxS{SR~ea04~k1hgKhvW8JyQ}w2PGxY1l zW~nK1@L?|EQ@`QF7HO-v4eISu^KZy+E7rr1%P(;xWlEWRe0un}q)I`Rp9xE9SfaeZ zhvcZl%L*SC)F~=N`KS<|m=Y!=eortFOHxTu`3cYpK9~0L(KUGwKsx*smR&S@0>lVM7O2UZU9~0=Ln3#$8?ZQbW ze-OSfOrA&ph$sLiRRGBp0D2Jt;uIuN72p|6CJ`!nTbYJ-SjR*Zlo*{704Bpoqg;vt zRCWAp4BKg7WA443DHE1_qX##pGudeWGapA z$$)|tAc#r;rUlCT(w+KDQ3~;yU{p{Pm^~E_s&XREX9Oh`5Ayf=(yY$km+mf$3xQBz z8cNV|VmUroBAcdf|=M z@%C20^}v^I#8DUoco@D5Z;o9U+cVa>dtBq9a$M3ZR07kA2REV_S`6XfW8qut0`Lw* zO|>$*Ktd2V-?pjXndNev7+zv{(g^XH()_Wo6rAMc5Lht^;-iVgqyo)JNjFvhLlQfI z-`e*8{E|6M7?7GFWVWG9Vrlpt=-)DGOhZ3Z6 z62?W{JJIIty)Xj~llGI8TOX`)mLRHtd3hMhu&D$8CNn?e0N^>6@KHeWV^WxdF{jUy<_?YtAQ3pSu-2gTB zeL`su;CT3yIshQ!OJ`NFs_TQ;N>x|U*_G$I)&Ng54%}URNbUMT$6@>`v2Qx)NacWW zZnl?#n4Ls521o?A@IX;51|nqIMJUUV^APA}$SixFz#*~e?G+ONuTtR%Fw#v6 zQCZ|qjSLU@&z|sc8W#a))y&a^AS#*%@y8`pcR!^GsySX4qEqEIsHqnODySyDtfp#I z6V8VN(go0?FxZAvXl(PO#Bt{OTQ=7f$D(7YYsJ>M>hXSa%T;~D#TQ)s2Yd6*eRm0C zuD|K2E3r_oD=wJ#!}>;&ZR%qG%yhA^GB|eSKCR-ub*=KS7Dzc5FkvE^L{`6g1Tbk+ z#`GwYHXLb=&9D*V@IVncL;uv0VMKGjA%8Dund-$LcH4Ky36gJMk`0}8i;!cL{Yy+5P_wCzv2hTh0eq(Ir z!r081GLBI%ZQ94LGd=em3aV3&FlYjm_|owB_+o4Xwq7TajTqAeF^0hybIn9g0HdL6 z5NH$?;sYQsT53#G!4>jl6Ql>Ao!*Idh}Vo^hmQyo=)+1QfO3@Z9?0%ke&tW!zxMr) z&i~@Yg7?6;Yc?4CVf}U{Yu(OFbn4}cBR(@d4S3C=r~*~-(ga@-|W^Ns~>)!T5j=iQ#4_b%90o!%SHZ3XAH?9__0ZPnGBy#(I1IDE_P zxpHptTz<>b%eEEw{(A$f?)n>UzToC_b)UJPUhe#O&mWH58!dT2z}K~;rViASoyv{m zE@sm!-UG$z1M>r`&bp;bi-)f}`?7)MRH5bIC!K|szC71Q55ZbB8)gPcNQUmS4cxHq zu=BdZW=I4O50*zcq+ug?N2#1KW13JVg_w#n0>%dJW140tgno{jD;I+& z{oXN5q~uVF-^hRkh2OoA0tbu~c=t2>ZoCM}HKx7Lf;@QlRD3cHnd@%9!+}Qps-E8{ zJ$eTb!;p+5LOtJ+V}+eQU#os_8G2St@fAaT2=*dqM}RLBx(@;RipJq$H4DTwSgK}` zC*lbzL7rlf@Fr6<>&Ci=?mP$Na`kRP?lV#}FFk;bLZcGEc7C@2rlNW4CBqoU4}p!Z z1hzIKu+ae^^O^Lhq9+z~5D<@uY2|DvfDRRaSHq)HA6%)j9Y8szH4bJk88Pi|hyPyl z*`d?#*AyBDzumZ1+nKDho%z7p&fK?gT>Aepa_bwUr-14Iu(J!evwP9Hp*t%qzxbz@ zuD$fp!23+0q5E4pgaYs;nE3U3SZ*HiIW>zi6$;6-n9j^dRBJdk{PLMs`$t9wjt=$* z`p=#19~v1L9vaa+Bc}(>Y)06rB!~gb-!$v+NICcMoRMfu20ux}H_wb&gSg# z+?HHhc5ub%gITu#(ZAwsh31#_jbKc<{*}hTV*TKP<5pb*oJ8*Vm4@zOUH5`rzZ!mW zI6J!RE3|Ze9K7E0!m79Fwr9s}XWchn?<6&i?{Ck0e30*~uY?m*QIWrh53=&l1WELtNW1(ADo@C9 zAqu|==s6GwX}A?K7<6@wiwiMHvq^DLPd0RefYck!Yjf)DDKm$Jr_7lqfj~qe%!nXo zz?`7D;A~!Twyb*UZd*O?%)K>t!`cF-l8vue_W=^m_GHo7miHYhI1lByLmPK^AP^Je zcpz{G-7DSxngZ{)9zK~;!|>*=Jkt1HT<`uq{sPv|jq`rLU(eanCZ5;W&S#ahg~z~k zSYw}qj-#T5!@d^H8VJCzgg}6zvC}gEG%Mx}QTZap!=^z5xFOAodjt{m1?;hqQEA-8 zL?T*_o|tapQyb8jHtG7(m7&=J0n}w60CQ4`#zv*M#tC#{S~Dl999K0?nw3?}hGGNA z)e8T6647aiK94-@mB_2vDq6;O+%=->Nl)I^ZN0ICI2D!p=#OCtb7kdc0KvE!=5zAe z7sUBbV*eLuylWxN=b1cdxNEU^P4k{o9Wi_6kG^y2ty6i=sUmlJm8&wk{YCD?rtZli zH?XO@tH`xgx(ja>x#mhQ@40U<@7Y`A+Djf=jcMLp+G1j!&$1-~RgPQ3s^qXR_1Vr6 zfhq@YSg1<1ZsrI?0PlMPB?8UmU29mC_?>5_3LB`7TX2lYLDyObt1)Jhr z3+d}@(?7YY&#jJy&MOBO5B_Lw>C$y;<4v18Uv+53*1Kw}USO}-7Hvy4mz}rk8m^vv z_hj~!?5j(IcL`@c#Vpuvx~i8Ne>!x-)mm`17G1j*n45LGa<3HYb}!g(dRue*i{3V% yvbmQ`OZ%^OzuTSNx%5mnlsmtyA>3B|%>Dz&OO=!W literal 0 HcmV?d00001 diff --git a/caveman/caveman-compress/scripts/__pycache__/validate.cpython-312.pyc b/caveman/caveman-compress/scripts/__pycache__/validate.cpython-312.pyc new file mode 100755 index 0000000000000000000000000000000000000000..528a65a264154496c4aaf284c505a0884eee7cdf GIT binary patch literal 9467 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 literal 0 HcmV?d00001 diff --git a/caveman/caveman-compress/scripts/benchmark.py b/caveman/caveman-compress/scripts/benchmark.py new file mode 100755 index 0000000..f9e2ee0 --- /dev/null +++ b/caveman/caveman-compress/scripts/benchmark.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +from pathlib import Path +import sys + +# Support both direct execution and module import +try: + from .validate import validate +except ImportError: + sys.path.insert(0, str(Path(__file__).parent)) + from validate import validate + +try: + import tiktoken + _enc = tiktoken.get_encoding("o200k_base") +except ImportError: + _enc = None + + +def count_tokens(text): + if _enc is None: + return len(text.split()) # fallback: word count + return len(_enc.encode(text)) + + +def benchmark_pair(orig_path: Path, comp_path: Path): + orig_text = orig_path.read_text() + comp_text = comp_path.read_text() + + orig_tokens = count_tokens(orig_text) + comp_tokens = count_tokens(comp_text) + saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0 + result = validate(orig_path, comp_path) + + return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid) + + +def print_table(rows): + print("\n| File | Original | Compressed | Saved % | Valid |") + print("|------|----------|------------|---------|-------|") + for r in rows: + print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'✅' if r[4] else '❌'} |") + + +def main(): + # Direct file pair: python3 benchmark.py original.md compressed.md + if len(sys.argv) == 3: + orig = Path(sys.argv[1]).resolve() + comp = Path(sys.argv[2]).resolve() + if not orig.exists(): + print(f"❌ Not found: {orig}") + sys.exit(1) + if not comp.exists(): + print(f"❌ Not found: {comp}") + sys.exit(1) + print_table([benchmark_pair(orig, comp)]) + return + + # Glob mode: repo_root/tests/caveman-compress/ + # __file__ lives at /skills/caveman-compress/scripts/benchmark.py + # Walk up four dirs: scripts → caveman-compress → skills → repo_root. + tests_dir = Path(__file__).resolve().parents[3] / "tests" / "caveman-compress" + if not tests_dir.exists(): + print(f"❌ Tests dir not found: {tests_dir}") + sys.exit(1) + + rows = [] + for orig in sorted(tests_dir.glob("*.original.md")): + comp = orig.with_name(orig.stem.removesuffix(".original") + ".md") + if comp.exists(): + rows.append(benchmark_pair(orig, comp)) + + if not rows: + print("No compressed file pairs found.") + return + + print_table(rows) + + +if __name__ == "__main__": + main() diff --git a/caveman/caveman-compress/scripts/cli.py b/caveman/caveman-compress/scripts/cli.py new file mode 100755 index 0000000..c314f87 --- /dev/null +++ b/caveman/caveman-compress/scripts/cli.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Caveman Compress CLI + +Usage: + caveman +""" + +import sys + +# Force UTF-8 on stdout/stderr before any code can print. Windows consoles +# default to cp1252 and crash on the ❌ glyphs in error/validation branches, +# masking the real error and leaving the user with a half-compressed file. +for _stream in (sys.stdout, sys.stderr): + reconfigure = getattr(_stream, "reconfigure", None) + if callable(reconfigure): + try: + reconfigure(encoding="utf-8", errors="replace") + except Exception: + pass + +from pathlib import Path + +from .compress import compress_file +from .detect import detect_file_type, should_compress + + +def print_usage(): + print("Usage: caveman ") + + +def main(): + if len(sys.argv) != 2: + print_usage() + sys.exit(1) + + filepath = Path(sys.argv[1]) + + # Check file exists + if not filepath.exists(): + print(f"❌ File not found: {filepath}") + sys.exit(1) + + if not filepath.is_file(): + print(f"❌ Not a file: {filepath}") + sys.exit(1) + + filepath = filepath.resolve() + + # Detect file type + file_type = detect_file_type(filepath) + + print(f"Detected: {file_type}") + + # Check if compressible + if not should_compress(filepath): + print("Skipping: file is not natural language (code/config)") + sys.exit(0) + + print("Starting caveman compression...\n") + + try: + success = compress_file(filepath) + + if success: + print("\nCompression completed successfully") + backup_path = filepath.with_name(filepath.stem + ".original.md") + print(f"Compressed: {filepath}") + print(f"Original: {backup_path}") + sys.exit(0) + else: + print("\n❌ Compression failed after retries") + sys.exit(2) + + except KeyboardInterrupt: + print("\nInterrupted by user") + sys.exit(130) + + except Exception as e: + print(f"\n❌ Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/caveman/caveman-compress/scripts/compress.py b/caveman/caveman-compress/scripts/compress.py new file mode 100755 index 0000000..7adc4fb --- /dev/null +++ b/caveman/caveman-compress/scripts/compress.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +""" +Caveman Memory Compression Orchestrator + +Usage: + python scripts/compress.py +""" + +import os +import re +import subprocess +from pathlib import Path +from typing import List + +OUTER_FENCE_REGEX = re.compile( + r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL +) + +# Filenames and paths that almost certainly hold secrets or PII. Compressing +# them ships raw bytes to the Anthropic API — a third-party data boundary that +# developers on sensitive codebases cannot cross. detect.py already skips .env +# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would +# slip through the natural-language filter. This is a hard refuse before read. +SENSITIVE_BASENAME_REGEX = re.compile( + r"(?ix)^(" + r"\.env(\..+)?" + r"|\.netrc" + r"|credentials(\..+)?" + r"|secrets?(\..+)?" + r"|passwords?(\..+)?" + r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?" + r"|authorized_keys" + r"|known_hosts" + r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)" + r")$" +) + +SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"}) + +SENSITIVE_NAME_TOKENS = ( + "secret", "credential", "password", "passwd", + "apikey", "accesskey", "token", "privatekey", +) + + +def is_sensitive_path(filepath: Path) -> bool: + """Heuristic denylist for files that must never be shipped to a third-party API.""" + name = filepath.name + if SENSITIVE_BASENAME_REGEX.match(name): + return True + lowered_parts = {p.lower() for p in filepath.parts} + if lowered_parts & SENSITIVE_PATH_COMPONENTS: + return True + # Normalize separators so "api-key" and "api_key" both match "apikey". + lower = re.sub(r"[_\-\s.]", "", name.lower()) + return any(tok in lower for tok in SENSITIVE_NAME_TOKENS) + + +def strip_llm_wrapper(text: str) -> str: + """Strip outer ```markdown ... ``` fence when it wraps the entire output.""" + m = OUTER_FENCE_REGEX.match(text) + if m: + return m.group(2) + return text + +from .detect import should_compress +from .validate import validate + +MAX_RETRIES = 2 + + +# ---------- Claude Calls ---------- + + +def call_claude(prompt: str) -> str: + api_key = os.environ.get("ANTHROPIC_API_KEY") + if api_key: + try: + import anthropic + + client = anthropic.Anthropic(api_key=api_key) + msg = client.messages.create( + model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"), + max_tokens=8192, + messages=[{"role": "user", "content": prompt}], + ) + return strip_llm_wrapper(msg.content[0].text.strip()) + except ImportError: + pass # anthropic not installed, fall back to CLI + # Fallback: use claude CLI (handles desktop auth) + try: + result = subprocess.run( + ["claude", "--print"], + input=prompt, + text=True, + capture_output=True, + check=True, + ) + return strip_llm_wrapper(result.stdout.strip()) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Claude call failed:\n{e.stderr}") + + +def build_compress_prompt(original: str) -> str: + return f""" +Compress this markdown into caveman format. + +STRICT RULES: +- Do NOT modify anything inside ``` code blocks +- Do NOT modify anything inside inline backticks +- Preserve ALL URLs exactly +- Preserve ALL headings exactly +- Preserve file paths and commands +- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file. + +Only compress natural language. + +TEXT: +{original} +""" + + +def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str: + errors_str = "\n".join(f"- {e}" for e in errors) + return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found. + +CRITICAL RULES: +- DO NOT recompress or rephrase the file +- ONLY fix the listed errors — leave everything else exactly as-is +- The ORIGINAL is provided as reference only (to restore missing content) +- Preserve caveman style in all untouched sections + +ERRORS TO FIX: +{errors_str} + +HOW TO FIX: +- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED +- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED +- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED +- Do not touch any section not mentioned in the errors + +ORIGINAL (reference only): +{original} + +COMPRESSED (fix this): +{compressed} + +Return ONLY the fixed compressed file. No explanation. +""" + + +# ---------- Core Logic ---------- + + +def compress_file(filepath: Path) -> bool: + # Resolve and validate path + filepath = filepath.resolve() + MAX_FILE_SIZE = 500_000 # 500KB + if not filepath.exists(): + raise FileNotFoundError(f"File not found: {filepath}") + if filepath.stat().st_size > MAX_FILE_SIZE: + raise ValueError(f"File too large to compress safely (max 500KB): {filepath}") + + # Refuse files that look like they contain secrets or PII. Compressing ships + # the raw bytes to the Anthropic API — a third-party boundary — so we fail + # loudly rather than silently exfiltrate credentials or keys. Override is + # intentional: the user must rename the file if the heuristic is wrong. + if is_sensitive_path(filepath): + raise ValueError( + f"Refusing to compress {filepath}: filename looks sensitive " + "(credentials, keys, secrets, or known private paths). " + "Compression sends file contents to the Anthropic API. " + "Rename the file if this is a false positive." + ) + + print(f"Processing: {filepath}") + + if not should_compress(filepath): + print("Skipping (not natural language)") + return False + + original_text = filepath.read_text(errors="ignore") + backup_path = filepath.with_name(filepath.stem + ".original.md") + + if not original_text.strip(): + print("❌ Refusing to compress: file is empty or whitespace-only.") + return False + + # Check if backup already exists to prevent accidental overwriting + if backup_path.exists(): + print(f"⚠️ Backup file already exists: {backup_path}") + print("The original backup may contain important content.") + print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.") + return False + + # Step 1: Compress + print("Compressing with Claude...") + compressed = call_claude(build_compress_prompt(original_text)) + + if compressed is None or not compressed.strip(): + print("❌ Compression aborted: Claude returned an empty response.") + print(" Original file is untouched (no backup created).") + return False + + if compressed.strip() == original_text.strip(): + print("❌ Compression aborted: output is identical to input.") + print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is") + print(" already in caveman form. Original file is untouched (no backup created).") + return False + + # Save original as backup, then verify the backup readback before + # touching the input file. If the filesystem dropped bytes (encoding, + # antivirus, disk full), unlink the bad backup and abort instead of + # leaving the user with a corrupt backup + compressed primary. + backup_path.write_text(original_text) + backup_readback = backup_path.read_text(errors="ignore") + if backup_readback != original_text: + print(f"❌ Backup write verification failed: {backup_path}") + print(" In-memory original differs from on-disk backup. Aborting before touching the input file.") + try: + backup_path.unlink() + except OSError: + pass + return False + filepath.write_text(compressed) + + # Step 2: Validate + Retry + for attempt in range(MAX_RETRIES): + print(f"\nValidation attempt {attempt + 1}") + + result = validate(backup_path, filepath) + + if result.is_valid: + print("Validation passed") + break + + print("❌ Validation failed:") + for err in result.errors: + print(f" - {err}") + + if attempt == MAX_RETRIES - 1: + # Restore original on failure + filepath.write_text(original_text) + backup_path.unlink(missing_ok=True) + print("❌ Failed after retries — original restored") + return False + + print("Fixing with Claude...") + compressed = call_claude( + build_fix_prompt(original_text, compressed, result.errors) + ) + filepath.write_text(compressed) + + return True diff --git a/caveman/caveman-compress/scripts/detect.py b/caveman/caveman-compress/scripts/detect.py new file mode 100755 index 0000000..8d5f6d7 --- /dev/null +++ b/caveman/caveman-compress/scripts/detect.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Detect whether a file is natural language (compressible) or code/config (skip).""" + +import json +import re +from pathlib import Path + +# Extensions that are natural language and compressible +COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"} + +# Extensions that are code/config and should be skipped +SKIP_EXTENSIONS = { + ".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml", + ".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml", + ".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c", + ".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua", + ".dockerfile", ".makefile", ".csv", ".ini", ".cfg", +} + +# Patterns that indicate a line is code +CODE_PATTERNS = [ + re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"), + re.compile(r"^\s*(def |class |function |async function |export )"), + re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"), + re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets + re.compile(r"^\s*@\w+"), # decorators/annotations + re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value + re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal +] + + +def _is_code_line(line: str) -> bool: + """Check if a line looks like code.""" + return any(p.match(line) for p in CODE_PATTERNS) + + +def _is_json_content(text: str) -> bool: + """Check if content is valid JSON.""" + try: + json.loads(text) + return True + except (json.JSONDecodeError, ValueError): + return False + + +def _is_yaml_content(lines: list[str]) -> bool: + """Heuristic: check if content looks like YAML.""" + yaml_indicators = 0 + for line in lines[:30]: + stripped = line.strip() + if stripped.startswith("---"): + yaml_indicators += 1 + elif re.match(r"^\w[\w\s]*:\s", stripped): + yaml_indicators += 1 + elif stripped.startswith("- ") and ":" in stripped: + yaml_indicators += 1 + # If most non-empty lines look like YAML + non_empty = sum(1 for l in lines[:30] if l.strip()) + return non_empty > 0 and yaml_indicators / non_empty > 0.6 + + +def detect_file_type(filepath: Path) -> str: + """Classify a file as 'natural_language', 'code', 'config', or 'unknown'. + + Returns: + One of: 'natural_language', 'code', 'config', 'unknown' + """ + ext = filepath.suffix.lower() + + # Extension-based classification + if ext in COMPRESSIBLE_EXTENSIONS: + return "natural_language" + if ext in SKIP_EXTENSIONS: + return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config" + + # Extensionless files (like CLAUDE.md, TODO) — check content + if not ext: + try: + text = filepath.read_text(errors="ignore") + except (OSError, PermissionError): + return "unknown" + + lines = text.splitlines()[:50] + + if _is_json_content(text[:10000]): + return "config" + if _is_yaml_content(lines): + return "config" + + code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l)) + non_empty = sum(1 for l in lines if l.strip()) + if non_empty > 0 and code_lines / non_empty > 0.4: + return "code" + + return "natural_language" + + return "unknown" + + +def should_compress(filepath: Path) -> bool: + """Return True if the file is natural language and should be compressed.""" + if not filepath.is_file(): + return False + # Skip backup files + if filepath.name.endswith(".original.md"): + return False + return detect_file_type(filepath) == "natural_language" + + +if __name__ == "__main__": + import sys + + if len(sys.argv) < 2: + print("Usage: python detect.py [file2] ...") + sys.exit(1) + + for path_str in sys.argv[1:]: + p = Path(path_str).resolve() + file_type = detect_file_type(p) + compress = should_compress(p) + print(f" {p.name:30s} type={file_type:20s} compress={compress}") diff --git a/caveman/caveman-compress/scripts/validate.py b/caveman/caveman-compress/scripts/validate.py new file mode 100755 index 0000000..dc07307 --- /dev/null +++ b/caveman/caveman-compress/scripts/validate.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +import re +from collections import Counter +from pathlib import Path + +URL_REGEX = re.compile(r"https?://[^\s)]+") +FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$") +HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE) +BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE) + +# crude but effective path detection +# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match +PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+") + + +class ValidationResult: + def __init__(self): + self.is_valid = True + self.errors = [] + self.warnings = [] + + def add_error(self, msg): + self.is_valid = False + self.errors.append(msg) + + def add_warning(self, msg): + self.warnings.append(msg) + + +def read_file(path: Path) -> str: + return path.read_text(errors="ignore") + + +# ---------- Extractors ---------- + + +def extract_headings(text): + return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)] + + +def extract_code_blocks(text): + """Line-based fenced code block extractor. + + Handles ``` and ~~~ fences with variable length (CommonMark: closing + fence must use same char and be at least as long as opening). Supports + nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick + content). + """ + blocks = [] + lines = text.split("\n") + i = 0 + n = len(lines) + while i < n: + m = FENCE_OPEN_REGEX.match(lines[i]) + if not m: + i += 1 + continue + fence_char = m.group(2)[0] + fence_len = len(m.group(2)) + open_line = lines[i] + block_lines = [open_line] + i += 1 + closed = False + while i < n: + close_m = FENCE_OPEN_REGEX.match(lines[i]) + if ( + close_m + and close_m.group(2)[0] == fence_char + and len(close_m.group(2)) >= fence_len + and close_m.group(3).strip() == "" + ): + block_lines.append(lines[i]) + closed = True + i += 1 + break + block_lines.append(lines[i]) + i += 1 + if closed: + blocks.append("\n".join(block_lines)) + # Unclosed fences are silently skipped — they indicate malformed markdown + # and including them would cause false-positive validation failures. + return blocks + + +def extract_urls(text): + return set(URL_REGEX.findall(text)) + + +def extract_paths(text): + return set(PATH_REGEX.findall(text)) + + +def count_bullets(text): + return len(BULLET_REGEX.findall(text)) + + +def extract_inline_codes(text): + text_without_fences = re.sub(r"^```[\s\S]*?^```", "", text, flags=re.MULTILINE) + text_without_fences = re.sub(r"^~~~[\s\S]*?^~~~", "", text_without_fences, flags=re.MULTILINE) + return re.findall(r"`([^`]+)`", text_without_fences) + + +# ---------- Validators ---------- + + +def validate_headings(orig, comp, result): + h1 = extract_headings(orig) + h2 = extract_headings(comp) + + if len(h1) != len(h2): + result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}") + + if h1 != h2: + result.add_warning("Heading text/order changed") + + +def validate_code_blocks(orig, comp, result): + c1 = extract_code_blocks(orig) + c2 = extract_code_blocks(comp) + + if c1 != c2: + result.add_error("Code blocks not preserved exactly") + + +def validate_urls(orig, comp, result): + u1 = extract_urls(orig) + u2 = extract_urls(comp) + + if u1 != u2: + result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}") + + +def validate_paths(orig, comp, result): + p1 = extract_paths(orig) + p2 = extract_paths(comp) + + if p1 != p2: + result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}") + + +def validate_bullets(orig, comp, result): + b1 = count_bullets(orig) + b2 = count_bullets(comp) + + if b1 == 0: + return + + diff = abs(b1 - b2) / b1 + + if diff > 0.15: + result.add_warning(f"Bullet count changed too much: {b1} -> {b2}") + + +def validate_inline_codes(orig, comp, result): + c1 = Counter(extract_inline_codes(orig)) + c2 = Counter(extract_inline_codes(comp)) + + if c1 != c2: + lost = set(c1.keys()) - set(c2.keys()) + added = set(c2.keys()) - set(c1.keys()) + for code, count in c1.items(): + if code in c2 and c2[code] < count: + lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)") + if lost: + result.add_error(f"Inline code lost: {lost}") + if added: + result.add_warning(f"Inline code added: {added}") + + +# ---------- Main ---------- + + +def validate(original_path: Path, compressed_path: Path) -> ValidationResult: + result = ValidationResult() + + orig = read_file(original_path) + comp = read_file(compressed_path) + + validate_headings(orig, comp, result) + validate_code_blocks(orig, comp, result) + validate_urls(orig, comp, result) + validate_paths(orig, comp, result) + validate_bullets(orig, comp, result) + validate_inline_codes(orig, comp, result) + + return result + + +# ---------- CLI ---------- + +if __name__ == "__main__": + import sys + + if len(sys.argv) != 3: + print("Usage: python validate.py ") + sys.exit(1) + + orig = Path(sys.argv[1]).resolve() + comp = Path(sys.argv[2]).resolve() + + res = validate(orig, comp) + + print(f"\nValid: {res.is_valid}") + + if res.errors: + print("\nErrors:") + for e in res.errors: + print(f" - {e}") + + if res.warnings: + print("\nWarnings:") + for w in res.warnings: + print(f" - {w}") diff --git a/caveman/caveman-help/README.md b/caveman/caveman-help/README.md new file mode 100755 index 0000000..5841256 --- /dev/null +++ b/caveman/caveman-help/README.md @@ -0,0 +1,38 @@ +# caveman-help + +Quick-reference card. One shot, no mode change. + +## What it does + +Prints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands. + +## How to invoke + +``` +/caveman-help +``` + +Also triggers on "caveman help", "what caveman commands", "how do I use caveman". + +## Example output + +``` +Modes: + /caveman full (default) + /caveman lite lighter + /caveman ultra extreme + /caveman wenyan classical Chinese + +Skills: + /caveman-commit terse Conventional Commits + /caveman-review one-line PR comments + /caveman-stats session token savings + +Deactivate: + "stop caveman" or "normal mode" +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — full reference card +- [Caveman README](../../README.md) — repo overview diff --git a/caveman/caveman-help/SKILL.md b/caveman/caveman-help/SKILL.md new file mode 100755 index 0000000..252221f --- /dev/null +++ b/caveman/caveman-help/SKILL.md @@ -0,0 +1,59 @@ +--- +name: caveman-help +description: > + Quick-reference card for all caveman modes, skills, and commands. + One-shot display, not a persistent mode. Trigger: /caveman-help, + "caveman help", "what caveman commands", "how do I use caveman". +--- + +# Caveman Help + +Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style. + +## Modes + +| Mode | Trigger | What change | +|------|---------|-------------| +| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. | +| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. | +| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. | +| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. | +| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. | +| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. | + +Mode stick until changed or session end. + +## Skills + +| Skill | Trigger | What it do | +|-------|---------|-----------| +| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. | +| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` | +| **caveman-compress** | `/caveman-compress ` | Compress .md files to caveman prose. Saves ~46% input tokens. | +| **caveman-help** | `/caveman-help` | This card. | + +## Deactivate + +Say "stop caveman" or "normal mode". Resume anytime with `/caveman`. + +## Configure Default Mode + +Default mode = `full`. Change it: + +**Environment variable** (highest priority): +```bash +export CAVEMAN_DEFAULT_MODE=ultra +``` + +**Config file** (`~/.config/caveman/config.json`): +```json +{ "defaultMode": "lite" } +``` + +Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`. + +Resolution: env var > config file > `full`. + +## More + +Full docs: https://github.com/JuliusBrussee/caveman diff --git a/caveman/caveman-review/README.md b/caveman/caveman-review/README.md new file mode 100755 index 0000000..acf519f --- /dev/null +++ b/caveman/caveman-review/README.md @@ -0,0 +1,33 @@ +# caveman-review + +One-line PR comments. Location, problem, fix. No throat-clearing. + +## What it does + +Generates code review comments in `L: . .` format. One line per finding. Severity emoji: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes. + +Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the *why*. Resumes terse for the rest. + +Output only — does not approve, request changes, or run linters. + +## How to invoke + +``` +/caveman-review +``` + +Also triggers on "review this PR", "code review", "review the diff". + +## Example output + +``` +L42: 🔴 bug: user can be null after .find(). Add guard before .email. +L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist. +L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3). +L107: ❓ q: why drop the cache here? Reads on next request will miss. +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview diff --git a/caveman/caveman-review/SKILL.md b/caveman/caveman-review/SKILL.md new file mode 100755 index 0000000..48f4adb --- /dev/null +++ b/caveman/caveman-review/SKILL.md @@ -0,0 +1,55 @@ +--- +name: caveman-review +description: > + Ultra-compressed code review comments. Cuts noise from PR feedback while preserving + the actionable signal. Each comment is one line: location, problem, fix. Use when user + says "review this PR", "code review", "review the diff", "/review", or invokes + /caveman-review. Auto-triggers when reviewing pull requests. +--- + +Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing. + +## Rules + +**Format:** `L: . .` — or `:L: ...` when reviewing multi-file diffs. + +**Severity prefix (optional, when mixed):** +- `🔴 bug:` — broken behavior, will cause incident +- `🟡 risk:` — works but fragile (race, missing null check, swallowed error) +- `🔵 nit:` — style, naming, micro-optim. Author can ignore +- `❓ q:` — genuine question, not a suggestion + +**Drop:** +- "I noticed that...", "It seems like...", "You might want to consider..." +- "This is just a suggestion but..." — use `nit:` instead +- "Great work!", "Looks good overall but..." — say it once at the top, not per comment +- Restating what the line does — the reviewer can read the diff +- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:` + +**Keep:** +- Exact line numbers +- Exact symbol/function/variable names in backticks +- Concrete fix, not "consider refactoring this" +- The *why* if the fix isn't obvious from the problem statement + +## Examples + +❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here." + +✅ `L42: 🔴 bug: user can be null after .find(). Add guard before .email.` + +❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability." + +✅ `L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.` + +❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case." + +✅ `L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).` + +## Auto-Clarity + +Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest. + +## Boundaries + +Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style. \ No newline at end of file diff --git a/caveman/caveman-stats/README.md b/caveman/caveman-stats/README.md new file mode 100755 index 0000000..30b28f9 --- /dev/null +++ b/caveman/caveman-stats/README.md @@ -0,0 +1,30 @@ +# caveman-stats + +Real session token receipts. No AI estimation. + +## What it does + +Reads the current Claude Code session log directly and reports actual input/output token usage plus estimated savings versus a non-caveman baseline. Numbers come from the JSONL session log on disk — the model itself does not compute or estimate them. Output is injected by the `caveman-mode-tracker` hook, which intercepts `/caveman-stats` and returns the formatted stats as a blocked-decision reason. + +Each run also writes a lifetime-savings suffix file used by the statusline badge (`⛏ 12.4k`). + +## How to invoke + +``` +/caveman-stats +``` + +## Example output + +``` +Session: 47 turns +Input: 12,304 tokens +Output: 3,891 tokens (caveman) +Baseline: 11,247 tokens (estimated without caveman) +Saved: 7,356 tokens (~65%) +``` + +## See also + +- [`SKILL.md`](./SKILL.md) — hook contract and mechanics +- [Caveman README](../../README.md) — repo overview diff --git a/caveman/caveman-stats/SKILL.md b/caveman/caveman-stats/SKILL.md new file mode 100755 index 0000000..a7348ac --- /dev/null +++ b/caveman/caveman-stats/SKILL.md @@ -0,0 +1,10 @@ +--- +name: caveman-stats +description: > + Show real token usage and estimated savings for the current session. + Reads directly from the Claude Code session log — no AI estimation. + Triggers on /caveman-stats. Output is injected by the mode-tracker hook; + the model itself does not compute the numbers. +--- + +This skill is delivered by `hooks/caveman-stats.js` (read by `hooks/caveman-mode-tracker.js` on `/caveman-stats`). The model does not need to do anything when this skill fires — the hook returns `decision: "block"` with the formatted stats as the reason. The user sees the numbers immediately. diff --git a/caveman/caveman/README.md b/caveman/caveman/README.md new file mode 100755 index 0000000..d749b83 --- /dev/null +++ b/caveman/caveman/README.md @@ -0,0 +1,48 @@ +# caveman + +Talk like smart caveman. Same brain, fewer tokens. + +## What it does + +Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped. + +Six intensity levels: + +| Level | What change | +|-------|-------------| +| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. | +| `full` | Default. Drop articles, fragments OK, short synonyms. | +| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. | +| `wenyan-lite` | Classical Chinese register, light compression. | +| `wenyan-full` | Maximum 文言文. 80-90% character reduction. | +| `wenyan-ultra` | Extreme classical compression. | + +Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part. + +## How to invoke + +``` +/caveman # full mode (default) +/caveman lite # lighter compression +/caveman ultra # extreme compression +/caveman wenyan # classical Chinese +stop caveman # back to normal prose +``` + +## Example output + +Question: "Why does my React component re-render?" + +Normal prose: +> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue. + +Caveman (full): +> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. + +Caveman (ultra): +> Inline obj prop → new ref → re-render. `useMemo`. + +## See also + +- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions +- [Caveman README](../../README.md) — repo overview, install, benchmarks diff --git a/caveman/caveman/SKILL.md b/caveman/caveman/SKILL.md new file mode 100755 index 0000000..073d6bb --- /dev/null +++ b/caveman/caveman/SKILL.md @@ -0,0 +1,74 @@ +--- +name: caveman +description: > + Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman + while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, + wenyan-lite, wenyan-full, wenyan-ultra. + Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", + "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested. +--- + +Respond terse like smart caveman. All technical substance stay. Only fluff die. + +## Persistence + +ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode". + +Default: **full**. Switch: `/caveman lite|full|ultra`. + +## Rules + +Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact. + +Pattern: `[thing] [action] [reason]. [next step].` + +Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." +Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" + +## Intensity + +| Level | What change | +|-------|------------| +| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | +| **full** | Drop articles, fragments OK, short synonyms. Classic caveman | +| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate | +| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register | +| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) | +| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse | + +Example — "Why React component re-render?" +- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." +- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." +- ultra: "Inline obj prop → new ref → re-render. `useMemo`." +- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。" +- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。" +- wenyan-ultra: "新參照→重繪。useMemo Wrap。" + +Example — "Explain database connection pooling." +- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." +- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." +- ultra: "Pool = reuse DB conn. Skip handshake → fast under load." +- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。" +- wenyan-ultra: "池reuse conn。skip handshake → fast。" + +## Auto-Clarity + +Drop caveman when: +- Security warnings +- Irreversible action confirmations +- Multi-step sequences where fragment order or omitted conjunctions risk misread +- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions) +- User asks to clarify or repeats question + +Resume caveman after clear part done. + +Example — destructive op: +> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. +> ```sql +> DROP TABLE users; +> ``` +> Caveman resume. Verify backup exist first. + +## Boundaries + +Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end. \ No newline at end of file diff --git a/caveman/find-skills/SKILL.md b/caveman/find-skills/SKILL.md new file mode 100755 index 0000000..114c663 --- /dev/null +++ b/caveman/find-skills/SKILL.md @@ -0,0 +1,142 @@ +--- +name: find-skills +description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill. +--- + +# Find Skills + +This skill helps you discover and install skills from the open agent skills ecosystem. + +## When to Use This Skill + +Use this skill when the user: + +- Asks "how do I do X" where X might be a common task with an existing skill +- Says "find a skill for X" or "is there a skill for X" +- Asks "can you do X" where X is a specialized capability +- Expresses interest in extending agent capabilities +- Wants to search for tools, templates, or workflows +- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.) + +## What is the Skills CLI? + +The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools. + +**Key commands:** + +- `npx skills find [query]` - Search for skills interactively or by keyword +- `npx skills add ` - Install a skill from GitHub or other sources +- `npx skills check` - Check for skill updates +- `npx skills update` - Update all installed skills + +**Browse skills at:** https://skills.sh/ + +## How to Help Users Find Skills + +### Step 1: Understand What They Need + +When a user asks for help with something, identify: + +1. The domain (e.g., React, testing, design, deployment) +2. The specific task (e.g., writing tests, creating animations, reviewing PRs) +3. Whether this is a common enough task that a skill likely exists + +### Step 2: Check the Leaderboard First + +Before running a CLI search, check the [skills.sh leaderboard](https://skills.sh/) to see if a well-known skill already exists for the domain. The leaderboard ranks skills by total installs, surfacing the most popular and battle-tested options. + +For example, top skills for web development include: +- `vercel-labs/agent-skills` — React, Next.js, web design (100K+ installs each) +- `anthropics/skills` — Frontend design, document processing (100K+ installs) + +### Step 3: Search for Skills + +If the leaderboard doesn't cover the user's need, run the find command: + +```bash +npx skills find [query] +``` + +For example: + +- User asks "how do I make my React app faster?" → `npx skills find react performance` +- User asks "can you help me with PR reviews?" → `npx skills find pr review` +- User asks "I need to create a changelog" → `npx skills find changelog` + +### Step 4: Verify Quality Before Recommending + +**Do not recommend a skill based solely on search results.** Always verify: + +1. **Install count** — Prefer skills with 1K+ installs. Be cautious with anything under 100. +2. **Source reputation** — Official sources (`vercel-labs`, `anthropics`, `microsoft`) are more trustworthy than unknown authors. +3. **GitHub stars** — Check the source repository. A skill from a repo with <100 stars should be treated with skepticism. + +### Step 5: Present Options to the User + +When you find relevant skills, present them to the user with: + +1. The skill name and what it does +2. The install count and source +3. The install command they can run +4. A link to learn more at skills.sh + +Example response: + +``` +I found a skill that might help! The "react-best-practices" skill provides +React and Next.js performance optimization guidelines from Vercel Engineering. +(185K installs) + +To install it: +npx skills add vercel-labs/agent-skills@react-best-practices + +Learn more: https://skills.sh/vercel-labs/agent-skills/react-best-practices +``` + +### Step 6: Offer to Install + +If the user wants to proceed, you can install the skill for them: + +```bash +npx skills add -g -y +``` + +The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts. + +## Common Skill Categories + +When searching, consider these common categories: + +| Category | Example Queries | +| --------------- | ---------------------------------------- | +| Web Development | react, nextjs, typescript, css, tailwind | +| Testing | testing, jest, playwright, e2e | +| DevOps | deploy, docker, kubernetes, ci-cd | +| Documentation | docs, readme, changelog, api-docs | +| Code Quality | review, lint, refactor, best-practices | +| Design | ui, ux, design-system, accessibility | +| Productivity | workflow, automation, git | + +## Tips for Effective Searches + +1. **Use specific keywords**: "react testing" is better than just "testing" +2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd" +3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills` + +## When No Skills Are Found + +If no relevant skills exist: + +1. Acknowledge that no existing skill was found +2. Offer to help with the task directly using your general capabilities +3. Suggest the user could create their own skill with `npx skills init` + +Example: + +``` +I searched for skills related to "xyz" but didn't find any matches. +I can still help you with this task directly! Would you like me to proceed? + +If this is something you do often, you could create your own skill: +npx skills init my-xyz-skill +``` diff --git a/html-slide-deck.skill b/html-slide-deck.skill new file mode 100755 index 0000000000000000000000000000000000000000..e0f49ac10d2a1bc6536cdb00ee446d0f1b1cb363 GIT binary patch literal 14538 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 영역에 슬라이드를 채워 넣으세요. + +--- + +## Technical Guide 보일러플레이트 + +```html + + + + + + {{TITLE}} + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + +
+ + + + + +
+
+
⌨ 키보드 단축키
+
+
→ / PgDn
다음 슬라이드
+
← / PgUp
이전 슬라이드
+
Home
첫 슬라이드로
+
End
마지막 슬라이드로
+
T
목차 사이드바 토글
+
Esc
모달/사이드바 닫기
+
+ +
+
+ + + + +``` + +--- + +## 핵심 동작 원리 + +### 슬라이드 트랜지션 +모든 슬라이드는 `position: absolute; inset: 0`으로 겹쳐 있고, `transform: translateX(±100%)`로 오프스크린에 숨겨집니다. `goToSlide()`는 `void el.offsetWidth`(reflow 강제)를 호출해 position 재설정 후 트랜지션을 시작하므로, 방향성 있는 슬라이드-인/아웃 애니메이션이 구현됩니다. + +### 사이드바 Push +`body.sb-open` 클래스가 토글되면 `#deck`, `#nav`, `#progress-bar`가 CSS `left: 300px`으로 밀려납니다. 사이드바 자체는 `translateX(-300px) → translateX(0)` 전환입니다. + +### SLIDE_META vs DOM 순서 +- JS 엔진은 `querySelectorAll('.slide')` 결과의 **배열 인덱스**를 사용 +- `SLIDE_META[i].idx`가 그 배열 인덱스와 일치해야 사이드바 클릭이 올바른 슬라이드로 이동 +- 슬라이드 div의 `data-idx` 속성은 **시각적 레퍼런스용**일 뿐, JS가 읽지 않음 diff --git a/html-slide-deck/references/layout-patterns.md b/html-slide-deck/references/layout-patterns.md new file mode 100755 index 0000000..23a10ba --- /dev/null +++ b/html-slide-deck/references/layout-patterns.md @@ -0,0 +1,224 @@ +# 콘텐츠 레이아웃 패턴 + +`sc-body` 안에서 사용하는 레이아웃 패턴 모음입니다. + +--- + +## 1. 단일 코드 블록 + +```html +
src/MyFile.cpp
+

{{설명 텍스트}}

+
+
+ MyFile.cpp — functionName() + C++ +
+
// 코드 여기에
+void MyFunction() {
+    // ...
+}
+
+``` + +**지원 언어**: `language-cpp`, `language-javascript`, `language-python`, `language-plaintext` 등 Highlight.js 지원 언어 모두 사용 가능. + +--- + +## 2. 화면을 채우는 코드 블록 (sc-body-flex 필요) + +```html + +
src/MyFile.cpp
+
+
+
+ MyFile.cpp + C++ +
+
+      // 긴 코드
+    
+
+
+``` + +--- + +## 3. 2×2 스텝 그리드 (단계별 절차) + +4단계 절차를 카드로 표현할 때 사용합니다. `sc-body-flex` 필요. + +```html + +

{{전체 설명}}

+
+ + +
+
+ ① + {{단계 제목}} +
+
{{코드 또는 내용}}
+
+ + + +
+``` + +**카드 수 조정**: 3개는 `grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr;`, 6개는 `1fr 1fr 1fr / 1fr 1fr`. + +--- + +## 4. 2컬럼 레이아웃 (테이블 + 코드) + +```html + +
config.ini
+
+ + +
+
📋 필드 구조
+ + + + + +
필드설명예시
Field1설명값
+
+ + +
+
✏️ 등록 방법
+
+
config.iniINI
+
; 코드
+
+
+ 💡 +
팁 제목팁 내용
+
+
+ +
+``` + +--- + +## 5. Callout 변형 + +```html + +
+ 💡 +
제목내용
+
+ + +
+ ⚠️ +
제목내용
+
+ + +
+ ✅ +
제목내용
+
+ + +
+ 🚨 +
제목내용
+
+``` + +--- + +## 6. 체크리스트 + +검증 단계나 확인 사항 나열에 사용합니다. + +```html +

{{서두}}

+
+
+
①
+
항목 제목상세 설명
+
+
+
②
+
항목 제목상세 설명
+
+
+``` + +--- + +## 7. Mermaid 다이어그램 + +```html +
+flowchart LR + A[시작] --> B{조건} + B -->|Yes| C[처리] + B -->|No| D[종료] + C --> D +
+``` + +**주의**: Mermaid는 `mermaid.initialize()`가 실행된 이후 DOM에 있어야 렌더링됩니다. +슬라이드가 처음부터 hidden 상태라면 첫 번째 슬라이드가 아닌 경우 렌더링이 지연될 수 있습니다. +해결: `mermaid.initialize({ startOnLoad: true })`가 보일러플레이트에 포함되어 있으므로 보통 정상 동작합니다. + +--- + +## 8. 플로우 & 파일 목록 (2열 Overview) + +전체 흐름과 관련 파일을 한 슬라이드에 나란히 보여줄 때 사용합니다. + +```html + +
+ + +
+
🔄 전체 플로우
+
+flowchart TD + A[단계 1] --> B[단계 2] --> C[단계 3] +
+
+ + +
+
📁 수정 파일
+ +
+
+
파일명.cpp
+
파일 역할 설명
+
+
+
+ +
+``` + +--- + +## 패턴 선택 가이드 + +| 콘텐츠 유형 | 권장 패턴 | +|------------|----------| +| 코드 설명 + 짧은 코드 | 패턴 1 (단일 코드 블록) | +| 긴 코드 전체 표시 | 패턴 2 (화면 채우는 코드) | +| 단계별 절차 (4단계) | 패턴 3 (2×2 스텝 그리드) | +| 설정 파일 필드 설명 | 패턴 4 (2컬럼 테이블+코드) | +| 주의사항/팁 강조 | 패턴 5 (Callout) | +| 체크리스트/검증 | 패턴 6 (체크리스트) | +| 전체 흐름 시각화 | 패턴 7 (Mermaid) | +| 흐름 + 파일 목록 | 패턴 8 (2열 Overview) | diff --git a/html-slide-deck/references/slide-types.md b/html-slide-deck/references/slide-types.md new file mode 100755 index 0000000..a24184a --- /dev/null +++ b/html-slide-deck/references/slide-types.md @@ -0,0 +1,134 @@ +# 슬라이드 타입별 HTML 템플릿 + +각 슬라이드 타입의 HTML 구조입니다. `` 위치에 삽입하세요. + +--- + +## 1. 제목 + 목차 슬라이드 (slide-title) + +프레젠테이션의 첫 슬라이드. 좌측 = 제목 정보, 우측 = 목차 그리드. + +```html +
+ +
+
{{CATEGORY}} · {{SUBTITLE}}
+
{{MAIN TITLE}}
+
{{DESCRIPTION}}
+
+
🖥 N 슬라이드
+
⏱ 약 X분
+
📁 X개 파일
+
+
+ +
+
📋 목차
+
+
+ 01 + {{TOC ITEM 1}} +
+
+ 02 + {{TOC ITEM 2}} +
+ +
+
+
+``` + +**팁**: 목차 항목이 많을수록 각 행이 작아집니다. 5개 이하일 때 가장 보기 좋습니다. `goToSlide(N)`의 N은 해당 섹션의 첫 번째 슬라이드 DOM 인덱스입니다. + +--- + +## 2. Step 헤더 슬라이드 (slide-step) + +새로운 단계(Step)를 시작할 때 사용하는 전환 슬라이드. 강조색 그라디언트 배경. + +```html +
+ +
1
+ Step 1 · {{CATEGORY}} +
{{STEP TITLE}}
+
{{STEP 설명 — 이 단계에서 무엇을 하는지 1~2문장}}
+
+ Task 1-1 · {{태스크명}} + Task 1-2 · {{태스크명}} +
+
+``` + +**배경색 선택 가이드**: 단계 순서에 따라 s1→s2→s3→s4→s5 순으로 사용하면 자연스러운 색상 흐름이 생깁니다. + +--- + +## 3. 콘텐츠 슬라이드 (slide-content) + +실제 내용이 담기는 슬라이드. 헤더 + 본문 영역으로 구성됩니다. + +### 기본 구조 + +```html +
+
+ Task 1-1 +

{{슬라이드 제목}}

+
+
+ +
+
+``` + +### 콘텐츠가 많은 경우 (flex 확장) + +```html +
+
+ Task 2-1 +

{{슬라이드 제목}}

+
+
+ +
+
+``` + +**`sc-body-flex` 사용 시기**: 코드 블록, 다단 레이아웃, 테이블 등이 화면 높이를 최대한 채워야 할 때. + +--- + +## 4. 엔딩 슬라이드 (slide-end) + +프레젠테이션 마지막 슬라이드. SLIDE_META에 **포함하지 않습니다**. + +```html +
+
+
+
End of Presentation
+
The End
+
+
{{프레젠테이션 제목}}
+
{{부제 — 예: "X단계 · Y개 Task · Z개 파일 수정"}}
+
+``` + +--- + +## 태스크 뱃지 색상 매핑 + +Step 헤더의 배경색과 Task 슬라이드의 뱃지 색상을 일치시키세요: + +| Step 배경 | Task 뱃지 클래스 | 색상 | +|-----------|-----------------|------| +| `bg-s1` | `b1` | 파랑 (#1D4ED8) | +| `bg-s2` | `b2` | 보라 (#7C3AED) | +| `bg-s3` | `b3` | 핑크 (#DB2777) | +| `bg-s4` | `b4` | 초록 (#059669) | +| `bg-s5` | `b5` | 주황 (#EA580C) | + +SLIDE_META의 `color`도 동일한 색상 코드를 사용하면 사이드바의 색상이 통일됩니다.