diff --git a/.gitignore b/.gitignore index bda2271..26f0990 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ plans/ work-journal-plugin/ llm-wiki-plugin/config.json +*/skills/*-workspace/ diff --git a/llm-wiki-plugin/.claude-plugin/plugin.json b/llm-wiki-plugin/.claude-plugin/plugin.json index 87ede4e..c8710da 100644 --- a/llm-wiki-plugin/.claude-plugin/plugin.json +++ b/llm-wiki-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "llm-wiki-plugin", "description": "LLM-maintained personal wiki with ingest, query, lint, update operations", - "version": "1.0.0", + "version": "1.1.0", "author": { "name": "Stefan Cho" } diff --git a/llm-wiki-plugin/README.md b/llm-wiki-plugin/README.md index 84021e9..92ab1c7 100644 --- a/llm-wiki-plugin/README.md +++ b/llm-wiki-plugin/README.md @@ -15,8 +15,8 @@ Andrej Karpathy의 LLM Wiki 패턴을 Claude Code 스킬로 구현. LLM이 raw ## Operations -### `wiki init` — 새 위키 도메인 생성 -도메인명, 설명, 소스 유형을 입력받아 위키 구조를 초기화합니다. +### `wiki init` — 새 위키 생성 +위키 이름, 설명, 소스 유형을 입력받아 위키 구조를 초기화합니다. ### `wiki ingest ` — 소스 추가 파일, URL, 텍스트를 읽어 위키 페이지로 컴파일합니다. 관련 페이지를 자동 업데이트하고 양방향 백링크를 생성합니다. @@ -34,17 +34,18 @@ broken links, orphan pages, missing backlinks, stale pages 등을 검출하고 ``` {wiki_root}/ -└── {domain}/ - ├── SCHEMA.md # 위키 규약 - ├── raw/ # 원본 소스 (불변) - └── wiki/ - ├── index.md # 페이지 카탈로그 - ├── overview.md # 도메인 요약 - ├── log.md # 작업 이력 (append-only) - └── pages/ # 위키 페이지 (flat) - └── {slug}.md +├── SCHEMA.md # 위키 규약 (Identity) +├── raw/ # 원본 소스 (불변) +└── wiki/ + ├── index.md # 페이지 카탈로그 + ├── overview.md # 위키 요약 + ├── log.md # 작업 이력 (append-only) + └── pages/ # 위키 페이지 (flat) + └── {slug}.md ``` +위키는 단일 그래프로 운영됩니다. `[[slug]]` 교차 참조가 한 그래프 안에서만 유효하므로 위키를 여러 개로 쪼개지 않습니다. + ## Configuration 최초 실행 시 위키 루트 경로를 설정합니다. 설정은 `config.json`에 저장됩니다. diff --git a/llm-wiki-plugin/skills/llm-wiki/PRINCIPLES.md b/llm-wiki-plugin/skills/llm-wiki/PRINCIPLES.md index 0204c2e..32ca247 100644 --- a/llm-wiki-plugin/skills/llm-wiki/PRINCIPLES.md +++ b/llm-wiki-plugin/skills/llm-wiki/PRINCIPLES.md @@ -1,85 +1,75 @@ # LLM Wiki Principles -## 3-Layer Architecture - -### Layer 1: Raw Sources (`raw/`) -- **Immutable** — 한 번 저장된 소스는 절대 수정하지 않는다 -- 논문, 기사, URL 스냅샷, 트랜스크립트, 노트 등 원본 자료 -- 형식: `raw/{slug}.md` 또는 `raw/{slug}.pdf` -- LLM은 읽기만 가능, 쓰기 불가 - -### Layer 2: Wiki (`wiki/`) -- LLM이 생성하고 유지보수하는 컴파일된 지식 -- 구조: - - `wiki/index.md` — 전체 페이지 카탈로그 (매 operation마다 갱신) - - `wiki/overview.md` — 도메인 전체 요약 (이해가 바뀔 때 갱신) +각 operation은 ``로 필요한 섹션만 명시한다. 미리 다 읽지 않는다. + +## architecture + +위키는 단일 그래프다. `[[slug]]` 교차 참조가 한 그래프 안에서만 유효하므로 위키를 쪼개지 않는다. + +- **`raw/`** — 원본 자료 (논문/기사/URL/트랜스크립트/노트). `raw/{slug}.md` 또는 `raw/{slug}.{ext}` (PDF·이미지 등 binary는 동반 `raw/{slug}.meta.md`에 메타 헤더). 추가만 가능, 수정/삭제 금지. +- **`wiki/`** — LLM이 컴파일·유지보수. + - `wiki/index.md` — 페이지 카탈로그 + - `wiki/overview.md` — 위키 요약 - `wiki/log.md` — append-only 작업 이력 - - `wiki/pages/{slug}.md` — 개별 위키 페이지 (flat 구조, 하위 디렉토리 없음) + - `wiki/pages/{slug}.md` — flat (하위 디렉토리 없음) +- **`SCHEMA.md`** — 위키 정체성. Phase 0의 발견 진입점. -### Layer 3: Schema (`SCHEMA.md`) -- 위키의 계약서. 도메인, 규약, 페이지 유형을 정의 -- 스킬이 위키를 발견하고 이해하는 진입점 +## page-types -## Page Frontmatter +- `source` — 원본 요약 (논문, 기사, 영상, 코드) +- `entity` — 사람, 조직, 도구, 기술 +- `concept` — 아이디어, 패턴, 원칙 +- `index`, `overview` — 시스템 페이지. 각 템플릿에서만 사용 -모든 위키 페이지는 YAML frontmatter로 시작한다: +`source` vs `entity` vs `concept`은 페이지가 "무엇을 다루는가"로 판단 — 원본 자체가 주제면 source, 인물/조직/도구가 주제면 entity, 그 외 추상 개념이면 concept. + +## frontmatter ```yaml --- -title: Page Title -slug: page-title -type: source | entity | concept +title: ... +slug: ... +type: source|entity|concept|index|overview created: YYYY-MM-DD updated: YYYY-MM-DD -sources: [slug1, slug2] -tags: [tag1, tag2] +sources: [slug, ...] # optional +tags: [tag, ...] # optional --- ``` -### 필수 필드 -- `title`: 페이지 제목 -- `slug`: 파일명과 동일 (확장자 제외) -- `type`: source(원본 요약), entity(사람/도구/조직), concept(아이디어/패턴) -- `created`: 생성일 -- `updated`: 최종 수정일 +필수: `title`, `slug`, `type`, `created`, `updated`. lint 스크립트는 이 5개만 검증한다. -### 선택 필드 -- `sources`: 이 페이지가 참조하는 소스 slug 목록 -- `tags`: 분류 태그 +## wikilink -## Wikilink Format +- `[[slug]]` → `wiki/pages/{slug}.md` +- `[[slug|표시]]` 커스텀 표시 +- `[[slug#anchor]]` 섹션 앵커 (slug 부분으로만 해석) +- 외부 URL은 일반 마크다운 링크 +- `slug` = 파일명에서 `.md` 제외 -- 기본: `[[slug]]` → `wiki/pages/{slug}.md`로 연결 -- 커스텀 표시: `[[slug|표시 텍스트]]` -- 위키 내부 링크에만 사용. 외부 URL은 일반 마크다운 링크 사용 +## bidirectional -## Bidirectional Linking Policy +A가 `[[B]]`를 쓰면 B의 Related 섹션에도 `[[A]]`가 있어야 한다. ingest/update가 페이지를 변경한 직후 `scripts/lint_wiki.py {wiki_root} --json`을 실행해 `missing_backlink`가 0이 될 때까지 보강한다. -- A 페이지가 B를 `[[B]]`로 참조하면, B 페이지의 Related 섹션에도 `[[A]]`가 있어야 함 -- Ingest 시 새 페이지 작성 후 반드시 **백링크 감사** 수행 -- Grep으로 전체 `wiki/pages/`를 스캔하여 누락된 역방향 링크 추가 +## slug-rules -## Slug Rules +정규식 `^[a-z0-9][a-z0-9-]{0,49}$` (소문자 영숫자·하이픈, 첫 글자 영숫자, 1~50자). -- lowercase-with-hyphens만 허용 -- 최대 50자 -- 특수문자 불가 (알파벳, 숫자, 하이픈만) -- 예: "Attention Is All You Need" → `attention-is-all-you-need` -- 충돌 시 숫자 접미사: `concept-2` +LLM이 직접 생성한다 — 한글/CJK는 의미가 통하는 영어로 transliterate (예: "어텐션 메커니즘" → `attention-mechanism`, "Café" → `cafe`). 기존 페이지와 충돌하면 `-2`, `-3`을 붙인다. 50자 제한은 충돌 접미사 포함 후의 길이. -## Immutability Rules +## immutability -1. **`raw/` 불변**: 소스 파일은 추가만 가능, 수정/삭제 불가 -2. **`log.md` append-only**: 기존 로그 항목을 수정하거나 삭제하지 않음. 항상 맨 아래에 추가 -3. **SCHEMA.md 보존**: 사용자 확인 없이 SCHEMA.md를 수정하지 않음 +1. `raw/` 추가만 (수정/삭제 금지) +2. `log.md` append-only — 기존 항목 수정/삭제 금지. **추가는 `scripts/append_log.py`** (단, 위키 최초 생성 시 init이 직접 작성하는 첫 항목은 예외) +3. `SCHEMA.md` 사용자 명시 확인 없이 수정 금지 +4. `wiki/pages/` flat — 하위 디렉토리 금지 -## Log Entry Format +## log-format -```markdown -## [YYYY-MM-DD] | -- Pages created: [[slug1]], [[slug2]] -- Pages updated: [[slug3]], [[slug4]] -- Backlinks added: +``` +scripts/append_log.py "" ``` -Operations: `init`, `ingest`, `query`, `lint`, `update` +- `` ∈ `init|ingest|query|lint|update` +- `` 한 줄 (개행 금지 — 스크립트가 거부) +- 본문 bullet은 stdin으로 전달 (heredoc) diff --git a/llm-wiki-plugin/skills/llm-wiki/SKILL.md b/llm-wiki-plugin/skills/llm-wiki/SKILL.md index 12a43c2..0893536 100644 --- a/llm-wiki-plugin/skills/llm-wiki/SKILL.md +++ b/llm-wiki-plugin/skills/llm-wiki/SKILL.md @@ -8,291 +8,36 @@ agent: general-purpose # LLM Wiki -Manages an LLM-maintained personal wiki using the 3-layer architecture (raw sources → wiki pages → schema). The wiki compiles knowledge from raw sources into interconnected markdown pages that accumulate over time. +3-layer (raw → wiki → schema) 개인 위키. 라우터다 — operation 본문은 `operations/{op}.md`에 있고, 의도 분류 후 그 파일만 Read한다. -Read [PRINCIPLES.md](PRINCIPLES.md) for linking rules, frontmatter conventions, and immutability rules. + -## Phase 0: Config Discovery +모든 op 시작 전 1회 수행. 결과로 `wiki_root`(절대 경로)와 `wiki_exists`(SCHEMA.md 존재 여부)를 갖춘다. -Every operation starts here. Find the wiki root path. +1. 플러그인 루트의 `config.json` 확인 (이 SKILL.md의 부모의 부모 디렉토리). 없으면 `AskUserQuestion`로 위키 루트 경로를 받는다 (빈 응답이면 `~/wiki`로 기본). 받은 값을 `{ "wiki_root": "" }`로 작성. +2. `wiki_root`의 `~`를 절대 경로로 확장. +3. `{wiki_root}/SCHEMA.md` 존재 여부를 확인 → `wiki_exists`. -1. Determine the plugin install directory (where this SKILL.md lives) -2. Check if `config.json` exists in the plugin root (2 levels up from SKILL.md): - - **Exists**: Read `wiki_root` value - - **Not exists**: Ask user with `AskUserQuestion`: - > 위키 루트 경로를 설정해주세요. 모든 위키 도메인이 이 경로 하위에 생성됩니다. - > 기본값: `~/wiki` - - Create `config.json` with the user's answer: `{ "wiki_root": "" }` -3. Expand `~` to absolute path + -```json -// config.json -{ "wiki_root": "~/wiki" } -``` + -## Phase 1: Domain Discovery - -After config is loaded, find the target wiki domain. - -1. Run `Glob {wiki_root}/*/SCHEMA.md` to find all domains -2. **No domains found**: - - If operation is `init` → proceed to init - - Otherwise → inform user: "위키가 없습니다. 먼저 `wiki init`을 실행해주세요." -3. **One domain found**: Use it automatically -4. **Multiple domains found**: Ask user to select with `AskUserQuestion`: - > 여러 위키 도메인이 있습니다. 어떤 도메인에서 작업할까요? - > 1. {domain_1} - > 2. {domain_2} -5. Read `SCHEMA.md` of selected domain for conventions -6. Set `{domain_path}` = `{wiki_root}/{domain}` — all subsequent paths in operations are relative to this absolute path - -## Operation: init - -Creates a new wiki domain under the wiki root. - -### Steps - -1. Ensure Phase 0 is complete (config.json exists) -2. Ask user with `AskUserQuestion`: - > 새 위키 도메인을 생성합니다. - > - **도메인명** (디렉토리명으로 사용, lowercase-with-hyphens): - > - **도메인 설명** (한 줄): - > - **소스 유형** (예: papers, articles, videos, code): -3. Create directory structure: - ``` - {wiki_root}/{domain}/ - ├── SCHEMA.md - ├── raw/ - └── wiki/ - ├── index.md - ├── overview.md - ├── log.md - └── pages/ - ``` -4. Generate files from templates: - - `SCHEMA.md` ← [templates/schema-template.md](templates/schema-template.md) (fill domain, description, source types, today's date) - - `wiki/index.md` ← [templates/index-template.md](templates/index-template.md) (fill domain, today's date) - - `wiki/overview.md` ← [templates/overview-template.md](templates/overview-template.md) (fill domain, description, today's date) - - `wiki/log.md` ← initial content: - ```markdown - # Wiki Log - - Append-only operation record. - - --- - - ## [{today}] init | {domain} - - Wiki created at `{wiki_root}/{domain}/` - ``` -5. Report to user: - > ✅ 위키 도메인 `{domain}` 생성 완료 - > - 경로: `{wiki_root}/{domain}/` - > - 다음 단계: `wiki ingest ` 로 소스를 추가하세요 - -## Operation: ingest - -Processes a new source and integrates it into the wiki. - -### Steps - -1. **Accept source** — determine source type from user input: - - File path → `Read` the file - - URL → `WebFetch` the content. If WebFetch unavailable, ask user to paste content - - Pasted text → use directly -2. **Save raw source**: - - Generate slug from source title (see PRINCIPLES.md slug rules) - - **Text/markdown content** → save to `{domain_path}/raw/{slug}.md` with metadata header: - ```markdown - --- - source_url: {url_if_applicable} - ingested: {today} - type: {article|paper|video|code|note} - --- - - {full_source_content} - ``` - - **Binary files (PDF, images)** → copy original to `{domain_path}/raw/{slug}.{ext}` preserving format, then create a companion `{domain_path}/raw/{slug}.meta.md` with the metadata header above (content field = file path reference) -3. **Surface takeaways** — present to user with `AskUserQuestion`: - > 📖 **{source_title}** 분석 완료 - > - > **핵심 포인트:** - > 1. {takeaway_1} - > 2. {takeaway_2} - > 3. {takeaway_3} - > - > **관련 엔티티/개념:** {entity_list} - > - > 강조하거나 제외할 내용이 있나요? (없으면 Enter) -4. **Write source summary page**: - - Use [templates/page-template.md](templates/page-template.md) - - `type: source` - - Include: URL/path, ingest date, 2-3 paragraph synthesis, key takeaways, entity/concept links -5. **Update entity/concept pages**: - - For each entity/concept mentioned in the source: - - `Grep` for existing page in `{domain_path}/wiki/pages/` - - **Exists**: Read page, append source to `sources` frontmatter, add new information to content, update `updated` date - - **Not exists**: Create new page with `type: entity` or `type: concept` -6. **Bidirectional backlink audit** (CRITICAL): - - For every `[[slug]]` in newly created/updated pages: - - Read target page - - If target doesn't link back → add `[[new-slug]]` to target's Related section - - `Grep` all files in `{domain_path}/wiki/pages/` for mentions of entities/concepts from the new source that lack `[[links]]` - - Add missing wikilinks -7. **Update index.md**: - - Read current `{domain_path}/wiki/index.md` - - Add entry under appropriate category (Sources/Entities/Concepts): - `- [[{slug}]] — {one-line summary} _(ingested {today})_` - - Update Recent section with latest 5 entries -8. **Update overview.md**: - - Read current `{domain_path}/wiki/overview.md` - - If new source shifts understanding: update Key Themes, add Open Questions - - Update Statistics counts - - Update `updated` frontmatter date -9. **Append to log.md** (no user confirmation needed): - ```markdown - ## [{today}] ingest | {source_title} - - Pages created: [[{slug1}]], [[{slug2}]] - - Pages updated: [[{slug3}]], [[{slug4}]] - - Backlinks added: {count} - ``` -10. **Report**: - > ✅ **{source_title}** ingest 완료 - > - 소스 페이지: `[[{slug}]]` - > - 생성된 페이지: {count}개 - > - 업데이트된 페이지: {count}개 - > - 추가된 백링크: {count}개 - -## Operation: query - -Searches the wiki and synthesizes an answer with citations. - -### Steps - -1. **Index scan**: - - Read `{domain_path}/wiki/index.md` completely - - Identify relevant pages from the user's question - - Prioritize wiki content over general knowledge -2. **Page retrieval**: - - Read full content of identified pages - - Follow `[[cross-references]]` up to 2 hops where relevant - - Build comprehensive context -3. **Synthesize answer**: - - Ground all claims in wiki sources using inline `[[slug]]` citations - - Note agreements/disagreements between pages - - Explicitly flag gaps: "위키에 {topic}에 대한 내용이 없습니다" - - Format appropriately: prose for facts, tables for comparisons, numbered steps for procedures -4. **Offer archival** with `AskUserQuestion`: - > 이 답변을 위키 페이지로 저장할까요? - > 제안 slug: `{suggested-slug}` - - **Yes**: Create page with `type: concept`, tags `[query, analysis]`, run mini-ingest (update index, log, backlinks) - - **No**: Log only: - ``` - ## [{today}] query | {question_summary} (not filed) - ``` - -## Operation: lint - -Audits wiki health and offers fixes. - -### Steps - -1. **Build inventory**: - - `Glob {domain_path}/wiki/pages/*.md` for all pages - - Read `{domain_path}/wiki/index.md` and `{domain_path}/wiki/overview.md` - - Collect: all slugs, all `[[slug]]` references, all frontmatter fields -2. **Run checks**: - - **🔴 Errors** (must fix): - - Broken links: `[[slug]]` with no corresponding file - - Missing frontmatter: absent `title`, `slug`, `type`, `created`, or `updated` - - Invalid slug: not lowercase-with-hyphens - - **🟡 Warnings** (should fix): - - Orphan pages: zero inbound links (exclude index, overview) - - Missing backlinks: A→B exists but B→A doesn't - - Stale pages: `updated` older than 90 days with temporal language ("current", "latest", "recent") - - **🔵 Info**: - - Missing concept pages: entity/concept mentioned 3+ times without dedicated page - - Index gaps: pages exist but not listed in index.md - -3. **Output report**: - > 📋 **Wiki Lint Report** ({today}) - > - > **Pages scanned:** {count} - > - > 🔴 **Errors ({count})** - > {error_list} - > - > 🟡 **Warnings ({count})** - > {warning_list} - > - > 🔵 **Info ({count})** - > {info_list} - -4. **Offer fixes** with `AskUserQuestion`: - > 자동 수정 가능한 항목이 {count}개 있습니다. 수정할까요? - > - Broken links 제거/수정 - > - Missing backlinks 추가 - > - Index 누락 항목 추가 - > - Missing frontmatter 채우기 - - If accepted: apply fixes, report what changed - - If declined: skip - -5. **Append to log.md**: - ``` - ## [{today}] lint | {error_count} errors, {warning_count} warnings, {info_count} info - Fixed: {list_of_fixes_or_none} - ``` - -## Operation: update - -Revises wiki pages when knowledge changes or contradictions arise. - -### Steps +| 발화 | op | +|---|---| +| "wiki init", "위키 초기화", "위키 만들어" | init | +| URL/파일 경로/"ingest"/"위키 추가"/"이거 읽어줘" — 단 wiki_root 안의 기존 파일 경로면 update 우선 | ingest | +| 위키 내용 질문/"wiki query"/"위키에서 찾아줘" | query | +| "wiki lint"/"위키 검사"/"위키 건강 체크" | lint | +| "wiki update"/"위키 수정"/특정 페이지 + 변경 의도 | update | -1. **Identify targets**: - - From user's request: specific page names, topics, or from lint recommendations - - `Grep` to locate relevant pages in `{domain_path}/wiki/pages/` -2. **Propose changes**: - - For each target page, show: - > **{page_title}** (`[[{slug}]]`) - > - 현재: `{current_text}` - > - 변경: `{proposed_text}` - > - 이유: {reason} - - Wait for user confirmation via `AskUserQuestion` -3. **Apply edits**: - - Use `Edit` to modify pages - - Update `updated` frontmatter date to today -4. **Contradiction check**: - - `Grep` all pages for claims related to the updated content - - Flag conflicting assertions - - Offer to update those pages too -5. **Update metadata**: - - Revise index.md summary if the page's purpose changed - - Update overview.md if the update shifts overall understanding -6. **Append to log.md**: - ``` - ## [{today}] update | [[{slug1}]], [[{slug2}]] - Reason: {explanation} - Changes: {brief_summary} - ``` +모호하면 `AskUserQuestion`으로 ingest/query/lint/update 중 선택. -## Operation Detection + -Parse user intent into an operation: + -| User says | Operation | -|-----------|-----------| -| "wiki init", "위키 초기화", "위키 만들어" | init | -| URL, file path, "ingest", "위키 추가", "이거 읽어줘" | ingest | -| Question about wiki content, "wiki query", "위키에서 찾아줘" | query | -| "wiki lint", "위키 검사", "위키 건강 체크" | lint | -| "wiki update", "위키 수정", specific page + change intent | update | +1. op이 `init`이 아닌데 `wiki_exists`가 false면 "위키가 없습니다. `wiki init`을 먼저 실행해주세요." 안내 후 종료. +2. `operations/{op}.md`를 Read. 파일의 절차를 그대로 따른다 (모든 op는 Phase 0 결과와 `wiki_exists=true`를 prereq로 가정 — init만 false에서 시작). +3. op 파일의 ``가 명시한 PRINCIPLES.md 섹션만, 그 시점에 Read. "섹션"은 `## ` 헤딩 한 블록 (다음 `## ...` 직전까지). -If ambiguous, ask with `AskUserQuestion`: -> 어떤 작업을 할까요? -> 1. ingest — 새 소스 추가 -> 2. query — 위키에서 검색 -> 3. lint — 위키 건강 검사 -> 4. update — 페이지 수정 + diff --git a/llm-wiki-plugin/skills/llm-wiki/operations/ingest.md b/llm-wiki-plugin/skills/llm-wiki/operations/ingest.md new file mode 100644 index 0000000..cffb140 --- /dev/null +++ b/llm-wiki-plugin/skills/llm-wiki/operations/ingest.md @@ -0,0 +1,94 @@ +# operation: ingest + +새 소스를 처리하고 위키에 통합한다. + +PRINCIPLES.md: frontmatter, wikilink, bidirectional, page-types, slug-rules. + +## 1. 소스 수락 + +사용자 입력으로부터: +- 파일 경로 → `Read` +- URL → `WebFetch`. 실패 시 사용자에게 본문 붙여넣기 요청 +- 텍스트 → 그대로 + +## 2. raw 저장 + +slug는 PRINCIPLES.md slug-rules에 따라 직접 생성 (한글/CJK는 transliterate, 충돌 시 `-2/-3`). + +- 텍스트/마크다운 → `{wiki_root}/raw/{slug}.md`에 헤더 + 전체 본문 Write +- PDF·이미지 등 binary → `{wiki_root}/raw/{slug}.{ext}`로 원본 복사 + `{wiki_root}/raw/{slug}.meta.md`에 헤더만 + +헤더: + +```yaml +--- +source_url: +ingested: +type: article|paper|video|code|note +--- +``` + +## 3. 핵심 시사점 합성 후 사용자 확인 + +소스를 분석해 핵심 포인트 3개와 관련 엔티티/개념 후보를 뽑는다. 그 다음 `AskUserQuestion`: + +> 📖 **{source_title}** 분석 완료 +> +> **핵심 포인트:** 1) ... 2) ... 3) ... +> **관련 엔티티/개념:** ... +> +> 강조/제외할 내용? (없으면 Enter) + +## 4. source 페이지 작성 + +`templates/page-template.md`를 참고해서 다음 정보를 담은 페이지를 `{wiki_root}/wiki/pages/{slug}.md`로 Write: + +- frontmatter: `type: source`, sources/tags 적절히 +- 소스 메타 (URL/경로, 수집일) +- 2-3 문단 합성 요약 +- 핵심 시사점 (사용자 피드백 반영) +- Related 섹션에 엔티티/개념 wikilink + +template은 placeholder의 의미를 보고 의미 있게 채운다 — 비워둔 키 포인트가 있으면 항목을 줄인다. + +## 5. 엔티티/개념 페이지 갱신 + +언급된 엔티티/개념마다 `Grep {wiki_root}/wiki/pages/`로 기존 페이지 검색: + +- 존재: `sources` frontmatter에 추가, 본문에 새 정보 반영, `updated` 갱신 +- 없음: `type: entity` 또는 `concept`로 신규 생성 (slug는 같은 규칙) + +## 6. 양방향 백링크 보강 + +PRINCIPLES.md bidirectional에 따라: + +- 새/갱신 페이지의 모든 `[[slug]]`에 대해 대상 페이지 Related에 역링크가 있는지 확인하고, 없으면 추가 +- 기존 페이지에서 새 엔티티/개념이 plain text로 언급된 곳이 있으면 wikilink로 변환 +- 검증: `Bash: scripts/lint_wiki.py {wiki_root} --json`에서 `missing_backlink`가 0이 될 때까지 보강 + +## 7. index.md 갱신 + +`{wiki_root}/wiki/index.md`: + +- 적절한 카테고리(Sources/Entities/Concepts)에 `- [[{slug}]] — {one-line} _(ingested {today})_` 추가 +- Recent 섹션은 최근 5개 항목만 유지 + +## 8. overview.md 갱신 + +새 소스가 위키 전체 이해를 바꾸는 경우에만 Key Themes / Open Questions를 갱신한다. Statistics 카운트와 frontmatter `updated`는 항상 갱신. + +## 9. 로그 + +```bash +cat < +EOF +``` + +## 10. 보고 + +> ✅ **{source_title}** ingest 완료 +> - 소스: `[[{slug}]]` +> - 생성/업데이트/백링크: c1/c2/c3 diff --git a/llm-wiki-plugin/skills/llm-wiki/operations/init.md b/llm-wiki-plugin/skills/llm-wiki/operations/init.md new file mode 100644 index 0000000..027ee91 --- /dev/null +++ b/llm-wiki-plugin/skills/llm-wiki/operations/init.md @@ -0,0 +1,57 @@ +# operation: init + +`wiki_root`에 새 위키를 생성한다. + +없음. (시스템 페이지만 만들고, 사용자 페이지는 만들지 않는다. log.md 첫 항목은 init이 직접 작성하는 bootstrap 예외 — `append_log.py`를 쓰려면 log.md가 먼저 있어야 한다.) + +## 1. 사용자에게 위키 정체성을 묻는다 + +`AskUserQuestion`: + +> 위키를 생성합니다. +> - **위키 이름** (예: "Stefan's Knowledge Wiki") +> - **위키 설명** (한 줄) +> - **소스 유형** (예: papers, articles, videos, code) + +세 항목 모두 필수. 빈 응답이면 한 번 더 묻는다. + +## 2. 디렉토리 생성 + +```bash +mkdir -p {wiki_root}/raw {wiki_root}/wiki/pages +``` + +## 3. 시스템 페이지 작성 + +`{today}` = 시스템 오늘 날짜 (`YYYY-MM-DD`). + +각 템플릿을 Read하고 `{wiki_title}`, `{description}`, `{source_types}`, `{date}` 변수를 치환해서 Write: + +- `templates/schema-template.md` → `{wiki_root}/SCHEMA.md` +- `templates/index-template.md` → `{wiki_root}/wiki/index.md` +- `templates/overview-template.md` → `{wiki_root}/wiki/overview.md` + +치환은 **단순 문자열 치환**이 아니라 의미를 이해하고 처리한다 — 사용자 입력에 `{date}` 같은 글자가 들어있으면 그대로 두고 템플릿 placeholder만 바꾼다. + +## 4. log.md 초기화 (bootstrap) + +`{wiki_root}/wiki/log.md`에 다음을 직접 Write: + +```markdown +# Wiki Log + +Append-only operation record. + +--- + +## [{today}] init | {wiki_title} +- Wiki created at `{wiki_root}/` +``` + +이후 모든 로그 추가는 `scripts/append_log.py`로만. + +## 5. 사용자 보고 + +> ✅ 위키 `{wiki_title}` 생성 완료 +> - 경로: `{wiki_root}/` +> - 다음: `wiki ingest `로 소스 추가 diff --git a/llm-wiki-plugin/skills/llm-wiki/operations/lint.md b/llm-wiki-plugin/skills/llm-wiki/operations/lint.md new file mode 100644 index 0000000..1eaae09 --- /dev/null +++ b/llm-wiki-plugin/skills/llm-wiki/operations/lint.md @@ -0,0 +1,58 @@ +# operation: lint + +위키 건강 감사. 검사는 결정론적 — `scripts/lint_wiki.py`가 전담한다. + +스크립트 출력 자체에는 PRINCIPLES.md 필요 없다. 자동 수정 수락 시 frontmatter, bidirectional, wikilink 추가 Read. + +## 1. lint 실행 + +```bash +scripts/lint_wiki.py {wiki_root} --json +``` + +JSON: `{ scanned, errors[], warnings[], info[] }`. 각 항목은 `{ check, page?, target?, fields?, ... }`. + +검사 항목 (스크립트가 코드화): +- errors: `broken_link`, `missing_frontmatter`, `invalid_slug` +- warnings: `orphan`, `missing_backlink`, `stale` +- info: `unindexed` + +## 2. 리포트 출력 + +> 📋 **Wiki Lint Report** ({today}) +> Pages scanned: {scanned} +> +> 🔴 Errors (n) — broken_link / missing_frontmatter / invalid_slug +> 🟡 Warnings (n) — orphan / missing_backlink / stale +> 🔵 Info (n) — unindexed +> +> 각 항목 한 줄: `{check} {page} {detail}` + +## 3. 자동 수정 제안 + +수정 가능한 카테고리가 있으면 `AskUserQuestion`: + +> 자동 수정 가능 항목 n개. 수정할까요? +> - broken_link 제거/올바른 slug로 교체 +> - missing_backlink 추가 +> - unindexed 항목 index.md에 추가 +> - missing_frontmatter 채우기 + + +PRINCIPLES.md frontmatter, bidirectional, wikilink Read. 그 다음: + +- broken_link → 해당 페이지에서 wikilink 제거 또는 올바른 slug로 Edit +- missing_backlink → 대상 페이지 Related 섹션에 `[[from]]` 추가 +- unindexed → index.md 적절한 카테고리에 `- [[{slug}]] — {one-line}` 추가 +- missing_frontmatter → 누락 필드 채움 (`created`/`updated`는 파일 mtime 사용, mtime을 못 얻으면 today) + +적용 후 lint를 재실행해 잔여 항목을 보고한다. + + +## 4. 로그 + +```bash +scripts/append_log.py {wiki_root} lint "{e} errors, {w} warnings, {i} info" +``` + +자동 수정을 적용했으면 stdin으로 `Fixed: ` 전달. diff --git a/llm-wiki-plugin/skills/llm-wiki/operations/query.md b/llm-wiki-plugin/skills/llm-wiki/operations/query.md new file mode 100644 index 0000000..59be71c --- /dev/null +++ b/llm-wiki-plugin/skills/llm-wiki/operations/query.md @@ -0,0 +1,46 @@ +# operation: query + +위키를 검색하고 인용 포함 답변을 합성한다. + +PRINCIPLES.md: wikilink, page-types (합성에 type 의미 필요). 아카이브를 수락하면 frontmatter, slug-rules, bidirectional 추가 Read. + +## 1. 인덱스 스캔 + +`{wiki_root}/wiki/index.md`를 Read하고 질문에서 관련 페이지를 식별한다. 일반 지식보다 위키 내용을 우선시한다. + +## 2. 페이지 회수 + +식별된 페이지 전체를 Read. 관련 있는 경우 `[[cross-references]]`를 최대 2 hop까지 따라간다. + +## 3. 답변 합성 + +- 모든 주장을 인라인 `[[slug]]` 인용으로 위키 소스에 근거시킨다 +- 페이지 간 동의/불일치를 명시 +- 위키에 없는 부분은 명시: "위키에 {topic} 내용 없음" +- 형식: 사실은 산문, 비교는 표, 절차는 번호 단계 + +## 4. 아카이빙 제안 + +`AskUserQuestion`: + +> 이 답변을 위키 페이지로 저장할까요? +> 제안 slug: `` (PRINCIPLES.md slug-rules 따라 LLM이 미리 생성) + + +1. PRINCIPLES.md frontmatter, slug-rules, bidirectional 추가 Read +2. `type: concept`, `tags: [query, analysis]`로 페이지를 `{wiki_root}/wiki/pages/{slug}.md`에 Write +3. ingest.md의 단계 7(index 갱신), 6(백링크 보강)를 수행 — 이 두 단계만 수행, overview는 갱신하지 않는다 +4. 로그: + ```bash + cat < + EOF + ``` + + + +```bash +scripts/append_log.py {wiki_root} query "{question_summary} (not filed)" +``` + diff --git a/llm-wiki-plugin/skills/llm-wiki/operations/update.md b/llm-wiki-plugin/skills/llm-wiki/operations/update.md new file mode 100644 index 0000000..a0cba2c --- /dev/null +++ b/llm-wiki-plugin/skills/llm-wiki/operations/update.md @@ -0,0 +1,46 @@ +# operation: update + +지식이 바뀌거나 모순이 발생했을 때 위키 페이지를 개정한다. + +PRINCIPLES.md: frontmatter, bidirectional, immutability. + +## 1. 대상 식별 + +사용자 요청에서 페이지/주제/lint 권고 등을 추출. `Grep {wiki_root}/wiki/pages/`로 후보를 모은다. + +## 2. 변경 제안 + +대상이 1-3개면 페이지마다, 4개 이상이면 묶어서 한 번에 `AskUserQuestion`: + +> **{page_title}** (`[[{slug}]]`) +> - 현재: ... +> - 변경: ... +> - 이유: ... + +## 3. 편집 적용 + +`Edit`으로 수정. frontmatter `updated` = 오늘 날짜로 갱신. wikilink 그래프가 바뀌면 PRINCIPLES.md bidirectional에 따라 백링크를 보강하고 `scripts/lint_wiki.py {wiki_root} --json`에서 `missing_backlink`가 0인지 확인. + +## 4. 모순 점검 + +갱신 내용과 관련된 주장에 대해 `Grep`. 충돌 페이지를 보고하고, 갱신 여부를 사용자에게 확인. + +## 5. 메타 갱신 + +- 페이지 목적이 바뀌었으면 `index.md` 요약 개정 +- 전체 이해가 바뀌었으면 `overview.md` 갱신 + frontmatter `updated` + +## 6. 로그 + +```bash +cat < +Changes: +EOF +``` + + +- `raw/` 수정 (immutability) +- `log.md` 기존 항목 수정/삭제 (append-only) +- `SCHEMA.md` 수정 (사용자 명시 확인 없이 금지) + diff --git a/llm-wiki-plugin/skills/llm-wiki/scripts/append_log.py b/llm-wiki-plugin/skills/llm-wiki/scripts/append_log.py new file mode 100755 index 0000000..c7369a8 --- /dev/null +++ b/llm-wiki-plugin/skills/llm-wiki/scripts/append_log.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Append an entry to wiki/log.md (append-only). + +Usage: + append_log.py + + must not contain newlines (would break the heading). +Optional body bullets via stdin. +""" +import datetime +import sys +from pathlib import Path + + +def main() -> int: + if len(sys.argv) != 4: + print("usage: append_log.py ", file=sys.stderr) + return 2 + + root = Path(sys.argv[1]).expanduser().resolve() + op, description = sys.argv[2], sys.argv[3] + if "\n" in description: + print("refuse: description must not contain newlines", file=sys.stderr) + return 2 + log = root / "wiki" / "log.md" + if not log.exists(): + print(f"refuse: {log} does not exist", file=sys.stderr) + return 1 + + today = datetime.date.today().isoformat() + body = sys.stdin.read() if not sys.stdin.isatty() else "" + + entry = f"\n## [{today}] {op} | {description}\n" + if body.strip(): + entry += body if body.endswith("\n") else body + "\n" + + with log.open("a", encoding="utf-8") as f: + f.write(entry) + print(f"ok: appended to {log}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/llm-wiki-plugin/skills/llm-wiki/scripts/lint_wiki.py b/llm-wiki-plugin/skills/llm-wiki/scripts/lint_wiki.py new file mode 100755 index 0000000..e128131 --- /dev/null +++ b/llm-wiki-plugin/skills/llm-wiki/scripts/lint_wiki.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Audit wiki health. + +Usage: + lint_wiki.py [--json] + +Checks: + errors: broken_link, missing_frontmatter, invalid_slug + warnings: orphan, missing_backlink, stale + info: unindexed + +Frontmatter parsing is intentionally permissive — only the 5 required scalar +fields (title, slug, type, created, updated) are read. Lists/multiline YAML +in optional fields are not parsed; the LLM handles those at write time. +""" +import datetime +import json +import re +import sys +from pathlib import Path + +SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,49}$") +# wikilink target may include #anchor; strip anchor before resolving as slug +WIKILINK_RE = re.compile(r"\[\[([^\]\|#]+)(?:#[^\]\|]+)?(?:\|[^\]]+)?\]\]") +REQUIRED_FM = ("title", "slug", "type", "created", "updated") +STALE_DAYS = 90 +TIME_WORDS = re.compile(r"current|latest|recent|now|today|최신|최근", re.I) + + +def parse_frontmatter(text: str): + """Return (fm_dict, body). Tolerates EOF without trailing newline. + + Only top-level scalar `key: value` lines are captured — list/multiline + YAML is ignored on purpose; the script consumers read scalars only. + """ + if not text.startswith("---\n"): + return {}, text + body_start = -1 + for marker in ("\n---\n", "\n---"): + idx = text.find(marker, 4) + if idx >= 0: + body_start = idx + len(marker) + fm_text = text[4:idx] + break + if body_start < 0: + return {}, text + fm = {} + for line in fm_text.split("\n"): + if line.startswith((" ", "\t", "-")): + continue # nested / list continuation — skip + if ":" not in line: + continue + k, _, v = line.partition(":") + fm[k.strip()] = v.strip() + return fm, text[body_start:] + + +def main() -> int: + args = [a for a in sys.argv[1:] if not a.startswith("--")] + json_out = "--json" in sys.argv[1:] + if not args: + print("usage: lint_wiki.py [--json]", file=sys.stderr) + return 2 + + root = Path(args[0]).expanduser().resolve() + pages_dir = root / "wiki" / "pages" + index_path = root / "wiki" / "index.md" + if not pages_dir.exists(): + print(f"refuse: {pages_dir} does not exist", file=sys.stderr) + return 1 + + pages = {} + inbound: dict = {} + for p in sorted(pages_dir.glob("*.md")): + text = p.read_text(encoding="utf-8") + fm, body = parse_frontmatter(text) + slug = p.stem + links = set(WIKILINK_RE.findall(body)) + pages[slug] = {"path": str(p), "fm": fm, "body": body, "links": links} + for tgt in links: + inbound.setdefault(tgt, set()).add(slug) + + # also count inbound from index/overview so they're not flagged orphan + for sys_page in (index_path, root / "wiki" / "overview.md"): + if sys_page.exists(): + for tgt in WIKILINK_RE.findall(sys_page.read_text(encoding="utf-8")): + inbound.setdefault(tgt, set()).add(sys_page.stem) + + errors, warnings, info = [], [], [] + today = datetime.date.today() + + for slug, p in pages.items(): + if not SLUG_RE.match(slug): + errors.append({"check": "invalid_slug", "page": slug}) + missing = [k for k in REQUIRED_FM if k not in p["fm"]] + if missing: + errors.append({"check": "missing_frontmatter", "page": slug, "fields": missing}) + for tgt in p["links"]: + if tgt not in pages: + errors.append({"check": "broken_link", "page": slug, "target": tgt}) + if slug not in inbound and slug not in ("index", "overview"): + warnings.append({"check": "orphan", "page": slug}) + for tgt in p["links"]: + if tgt in pages and slug not in pages[tgt]["links"]: + warnings.append({"check": "missing_backlink", "from": slug, "to": tgt}) + u = p["fm"].get("updated", "") + try: + ud = datetime.date.fromisoformat(u[:10]) + if (today - ud).days > STALE_DAYS and TIME_WORDS.search(p["body"]): + warnings.append({"check": "stale", "page": slug, "updated": u}) + except ValueError: + pass + + if index_path.exists(): + index_text = index_path.read_text(encoding="utf-8") + for slug in pages: + if slug in ("index", "overview"): + continue + if f"[[{slug}]]" not in index_text: + info.append({"check": "unindexed", "page": slug}) + + summary = {"scanned": len(pages), "errors": errors, "warnings": warnings, "info": info} + + if json_out: + print(json.dumps(summary, ensure_ascii=False, indent=2)) + return 0 + + print(f"# Wiki Lint Report ({today})") + print(f"Pages scanned: {len(pages)}\n") + print(f"## Errors ({len(errors)})") + for e in errors or [{"check": "(none)"}]: + print(f"- {e}") + print(f"\n## Warnings ({len(warnings)})") + for w in warnings or [{"check": "(none)"}]: + print(f"- {w}") + print(f"\n## Info ({len(info)})") + for i in info or [{"check": "(none)"}]: + print(f"- {i}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/llm-wiki-plugin/skills/llm-wiki/templates/index-template.md b/llm-wiki-plugin/skills/llm-wiki/templates/index-template.md index c736605..057ac6f 100644 --- a/llm-wiki-plugin/skills/llm-wiki/templates/index-template.md +++ b/llm-wiki-plugin/skills/llm-wiki/templates/index-template.md @@ -6,7 +6,7 @@ created: {date} updated: {date} --- -# {domain} Wiki Index +# {wiki_title} Index ## Sources diff --git a/llm-wiki-plugin/skills/llm-wiki/templates/overview-template.md b/llm-wiki-plugin/skills/llm-wiki/templates/overview-template.md index 09c2f77..bc68335 100644 --- a/llm-wiki-plugin/skills/llm-wiki/templates/overview-template.md +++ b/llm-wiki-plugin/skills/llm-wiki/templates/overview-template.md @@ -1,16 +1,16 @@ --- title: Overview slug: overview -type: index +type: overview created: {date} updated: {date} --- -# {domain} Wiki Overview +# {wiki_title} Overview ## Scope -{scope_description} +{description} ## Key Themes diff --git a/llm-wiki-plugin/skills/llm-wiki/templates/schema-template.md b/llm-wiki-plugin/skills/llm-wiki/templates/schema-template.md index 968b7f9..bf4e0c6 100644 --- a/llm-wiki-plugin/skills/llm-wiki/templates/schema-template.md +++ b/llm-wiki-plugin/skills/llm-wiki/templates/schema-template.md @@ -1,41 +1,16 @@ -# {domain} Wiki Schema +# {wiki_title} Schema ## Identity -- **Domain:** {domain_description} +- **Description:** {description} - **Created:** {date} - **Source types:** {source_types} -## Page Types -- **source**: 원본 자료 요약 (논문, 기사, 영상 등) -- **entity**: 사람, 조직, 도구, 기술 -- **concept**: 아이디어, 패턴, 원칙 +## Conventions -## Frontmatter Format -```yaml ---- -title: Page Title -slug: page-title -type: source | entity | concept -created: YYYY-MM-DD -updated: YYYY-MM-DD -sources: [slug1, slug2] -tags: [tag1, tag2] ---- -``` +이 위키는 LLM Wiki 플러그인의 표준 규약을 따른다. +페이지 유형, frontmatter 형식, slug 규칙, wikilink 문법, 양방향 링킹 정책, +불변성 규칙, 로그 항목 형식은 플러그인의 `PRINCIPLES.md`를 단일 정본으로 한다. -## Cross-References -- Internal links: `[[slug]]` or `[[slug|display text]]` -- slug = filename without `.md` -- All links must be bidirectional +## Wiki-Specific Notes -## Index Categories -- Sources -- Entities -- Concepts - -## Governance Rules (immutable) -1. `raw/` is immutable — never modify source files -2. `log.md` is append-only — never rewrite, only append -3. `index.md` updates with every operation adding/changing pages -4. `wiki/pages/` is flat — all pages as `{slug}.md`, NO subdirectories -5. `overview.md` reflects current synthesis across all sources +_(이 위키만의 추가 규약이 있으면 여기에 기록. 예: 특정 태그 컨벤션, 카테고리 변형 등)_