@kybird/llm-wiki 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,6 @@
2
2
  // 원본: TTSTextViewer/.agents/skills/wiki-search/scripts/search.js
3
3
  // 변경점:
4
4
  // - __dirname '../../../../' 4단 종속 제거 → findDocRoot(), findQmd() 사용.
5
- // - findstr(Windows 전용) → process.platform 분기 (win32=findstr, else=grep).
6
5
  // - 2026-08-29 (개선계획 0-1): "QMD 성공 시 grep 건너뛰기" 분기 제거. QMD가 유사도로
7
6
  // 뭐라도 물어오면 정확 문자열 매칭이 묻히는 구조라 색인이 싱싱할수록 재현율이
8
7
  // 떨어졌다(plan.md 5.1(1)). 시맨틱은 대체재가 아니라 보완재 — 둘 다 항상 돌린다.
@@ -14,7 +13,10 @@
14
13
  // - 2026-08-29 (개선계획 1-6): 수집(collect)과 렌더(render)를 분리. --json이면
15
14
  // {schemaVersion: 1, kind: 'search-results', ...} 봉투로 출력 — 스킬 프롬프트가
16
15
  // 사람용 출력 문자열에 깨지지 않게 하는 계약.
17
- const { execSync } = require('child_process');
16
+ // - 2026-09-09 (개선계획 5-1-1): qmd 호출을 execFileSync 인자 배열로 — 검색어의
17
+ // 메타문자가 셸로 새는 것을 원천 차단.
18
+ // - 2026-09-09 (개선계획 5-1-6): findstr/grep 외부 호출을 제거하고 프로세스 안
19
+ // 라인 스캐너로. findstr은 ACP 의존이라 CP949 머신에서 한국어 검색어가 0건.
18
20
  const path = require('path');
19
21
  const fs = require('fs');
20
22
  const { findDocRoot, loadConfig } = require('./find-doc-root');
@@ -52,7 +54,7 @@ function search(query, options = {}) {
52
54
  }
53
55
 
54
56
  function collectGrepResults(query, wikiPath, rawPath) {
55
- // 따옴표는 findstr/grep 인용 규칙을 깨므로 키워드에서 제거.
57
+ // 따옴표는 문서 본문에 나오는 캐럭터라 키워드에서 제외(회수율).
56
58
  const keywords = query.split(/\s+/)
57
59
  .map(k => k.replace(/"/g, ''))
58
60
  .filter(k => k.length > 1);
@@ -61,34 +63,30 @@ function collectGrepResults(query, wikiPath, rawPath) {
61
63
 
62
64
  const found = new Map(); // absPath → { keywords: Set, snippets: Map<lineText, true> }
63
65
  const searchDirs = [wikiPath, rawPath];
64
- const isWindows = process.platform === 'win32';
65
66
 
67
+ // 외부 grep/findstr을 쓰지 않는다 — findstr은 코드페이지(ACP) 의존이라 한국어
68
+ // Windows(CP949)에서 UTF-8 문서의 한국어 검색어가 비트 불일치로 조용히 0건이었다
69
+ // (2026-09-09 리뷰 5-1-6). 프로세스 안 라인 스캐너는 인코딩·플랫폼 무관하고
70
+ // 메타문자가 셸로 새는 경로도 원천히 없다. 코퍼스는 doc/ 규모라 속도 문제 없다.
66
71
  for (const dir of searchDirs) {
67
72
  if (!fs.existsSync(dir)) continue;
68
- for (const kw of keywords) {
73
+ for (const filePath of listMarkdownFiles(dir)) {
74
+ let lines;
69
75
  try {
70
- let grepCmd;
71
- if (isWindows) {
72
- // /C: 통짜 문자열 매칭(정규식 아님) — 에러 메시지의 . : ( 등이 그대로 걸린다.
73
- // /M(files-only) 쓰지 않는다 — 매칭 줄 자체가 스니펫이 된다.
74
- grepCmd = `findstr /S /I /C:"${kw}" "${path.join(dir, '*.md')}"`;
75
- } else {
76
- // -F 고정 문자열(정규식 메타문자 무력화), -n 줄번호, -i 대소문자무시
77
- grepCmd = `grep -rniF --include="*.md" -e "${kw}" "${dir}"`;
76
+ lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
77
+ } catch { continue; }
78
+ for (const line of lines) {
79
+ if (!line.trim()) continue;
80
+ const lower = line.toLowerCase();
81
+ for (const kw of keywords) {
82
+ if (lower.includes(kw.toLowerCase())) {
83
+ if (!found.has(filePath)) found.set(filePath, { keywords: new Set(), snippets: new Map() });
84
+ const entry = found.get(filePath);
85
+ entry.keywords.add(kw);
86
+ const snippet = line.trim();
87
+ if (!entry.snippets.has(snippet)) entry.snippets.set(snippet, true);
88
+ }
78
89
  }
79
- const output = execSync(grepCmd, { encoding: 'utf8' });
80
- for (const line of output.split(/\r?\n/)) {
81
- if (!line.trim()) continue;
82
- const { file, snippet } = splitGrepLine(line);
83
- if (!file) continue;
84
- const abs = path.resolve(file);
85
- if (!found.has(abs)) found.set(abs, { keywords: new Set(), snippets: new Map() });
86
- const entry = found.get(abs);
87
- entry.keywords.add(kw);
88
- if (snippet && !entry.snippets.has(snippet)) entry.snippets.set(snippet, true);
89
- }
90
- } catch (e) {
91
- // findstr/grep returns exit code 1 if no matches
92
90
  }
93
91
  }
94
92
  }
@@ -111,6 +109,19 @@ function collectGrepResults(query, wikiPath, rawPath) {
111
109
  return { keywords, files };
112
110
  }
113
111
 
112
+ // .md 재귀 수집 — grep -r / findstr /S 대응.
113
+ function listMarkdownFiles(dir) {
114
+ const out = [];
115
+ for (const name of fs.readdirSync(dir)) {
116
+ const abs = path.join(dir, name);
117
+ let stat;
118
+ try { stat = fs.statSync(abs); } catch { continue; }
119
+ if (stat.isDirectory()) out.push(...listMarkdownFiles(abs));
120
+ else if (name.toLowerCase().endsWith('.md')) out.push(abs);
121
+ }
122
+ return out;
123
+ }
124
+
114
125
  function collectQmdResults(query, config) {
115
126
  const qmdPath = findQmd();
116
127
  if (!qmdPath) return { available: false, collections: [] };
@@ -120,7 +131,10 @@ function collectQmdResults(query, config) {
120
131
  const results = [];
121
132
  for (const coll of collections) {
122
133
  try {
123
- const output = execSync(`node "${qmdPath}" search "${query}" -c ${coll}`, { encoding: 'utf8', timeout: 30000 });
134
+ // 검색어를 문자열에 끼워 넣지 않는다 인자 배열은 메타문자를 데이터로만
135
+ // 전달한다(따옴표·$()·백틱 포함 질의가 에러 메시지 붙여넣기의 일상이다).
136
+ const output = execFileSync(process.execPath, [qmdPath, 'search', query, '-c', coll],
137
+ { encoding: 'utf8', timeout: 30000 });
124
138
  if (!isQmdEmpty(output)) results.push({ collection: coll, output: output.trim() });
125
139
  } catch (error) {
126
140
  // collection missing or QMD error for this collection — try next
@@ -171,16 +185,4 @@ function isQmdEmpty(output) {
171
185
  return /^no\s+results\s+found\.\s*$/i.test(output.trim());
172
186
  }
173
187
 
174
- // findstr/grep 출력 행에서 경로와 매칭 줄을 분리한다.
175
- // findstr: <경로>.md:<내용>
176
- // grep -n: <경로>.md:<줄번호>:<내용>
177
- // 경계는 첫 `.md:`로 판정 — 윈도우 절대경로의 드라이브 콜론(`D:`)과 충돌을 피한다.
178
- function splitGrepLine(line) {
179
- const idx = line.indexOf('.md:');
180
- if (idx === -1) return { file: null, snippet: null };
181
- const file = line.slice(0, idx + 3);
182
- const snippet = line.slice(idx + 4).replace(/^\d+:/, ''); // POSIX grep 줄번호 제거
183
- return { file, snippet: snippet.trim() };
184
- }
185
-
186
188
  module.exports = { search };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybird/llm-wiki",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "LLM-friendly knowledge graph + kanban for AI coding agents. Raw daily logs → compiled wiki → merged semantic+grep search, and a card-per-file kanban the work-loop skill consumes unattended. Drop-in for any repo.",
5
5
  "license": "MIT",
6
6
  "author": "kybird",
@@ -36,5 +36,8 @@
36
36
  "agentic-memory",
37
37
  "claude-code",
38
38
  "work-loop"
39
- ]
39
+ ],
40
+ "scripts": {
41
+ "test": "node --test test/roundtrip.test.js test/kanban-write.test.js test/kanban-pick-guards.test.js test/kanban-wait.test.js test/find-doc-root-worktree.test.js test/auto-update.test.js"
42
+ }
40
43
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: work-loop
3
3
  description: Unattended kanban loop — pick a card, resolve it with objective evidence, park judgment calls and move on. Board is the only task list; converge, don't diverge.
4
- skill-version: 4
4
+ skill-version: 5
5
5
  ---
6
6
  # When to use
7
7
 
@@ -27,7 +27,7 @@ pick → work → 판정 ─ done / handoff / abandon / supersede
27
27
  집을 카드 없음 → review(대기) 큐 점검 → 전부 대기면 질문을 모아 정지 (반스래시)
28
28
  ```
29
29
 
30
- # Rules (all six are load-bearing)
30
+ # Rules (all seven are load-bearing)
31
31
 
32
32
  0. **Condition-gated cards.** Some cards must not start yet:
33
33
  - `not_before: YYYY-MM-DD` in frontmatter (future date) — `pick` skips them
@@ -70,6 +70,14 @@ pick → work → 판정 ─ done / handoff / abandon / supersede
70
70
  On a long card, `llm-wiki card edit <제목> --renew-claim` before the timeout, or
71
71
  another loop instance will reclaim the card under you.
72
72
 
73
+ 7. **Worktrees share one board.** The board is a project resource, not a branch resource:
74
+ from a linked worktree, every `pick`/`done`/`card` writes the **primary worktree's**
75
+ `doc/kanban/`. If this loop runs in a linked worktree, its card changes appear as
76
+ uncommitted changes in the primary worktree — leave them there (the primary's next
77
+ commit picks them up); do not chase them into this worktree's commits, and do not
78
+ commit in the primary from here. `LLM_WIKI_WORKTREE_LOCAL=1` restores per-worktree
79
+ boards.
80
+
73
81
  # Splitting (supersede) — divergence guard
74
82
 
75
83
  Supersede only when each child is **strictly smaller** than the parent in context needed.
@@ -0,0 +1,28 @@
1
+ # AGENTS.md
2
+
3
+ <!-- Seeded by `llm-wiki init`. This file is YOURS — edit freely; init never overwrites it. -->
4
+
5
+ This repo uses **llm-wiki**: `doc/` is the knowledge base, `doc/kanban/` is the work board.
6
+ Files are the source of truth. Kanban card files are written ONLY via the CLI — people read them.
7
+
8
+ ## Skill usage — when to use what
9
+
10
+ | Moment | Skill / command |
11
+ |---|---|
12
+ | Before starting any task | `llm-wiki search "<keywords>"` — paste the verbatim error string when debugging |
13
+ | After fixing a bug / making a decision / discovering something | `wiki-log` skill → a Case in `doc/raw/YYYY-MM-DD.md` with verbatim error + `hash:` grounding |
14
+ | When raw logs have accumulated | `wiki-compile` skill → promote to `doc/wiki/` pages, then `llm-wiki compile index` |
15
+ | Sanity check of the knowledge base | `wiki-lint` skill or `llm-wiki lint` |
16
+ | Planning work (person present) | `kanban-plan` skill → cards via `llm-wiki card new "<title>"` |
17
+ | Unattended execution | `work-loop` skill → `llm-wiki pick --claim <name>`, park judgment calls with `handoff` |
18
+
19
+ ## Rules
20
+
21
+ - Quote error messages **character-for-character** in logs and wiki pages — `llm-wiki lint`
22
+ back-checks every quote and `hash:` against `doc/raw/`.
23
+ - Follow `status: deprecated` → `superseded_by` when reading wiki pages.
24
+ - The board is a **project** resource, not a branch resource: from any git linked worktree,
25
+ `llm-wiki` reads and writes the **primary worktree's** `doc/kanban/`. `pick`/`done` run in
26
+ a secondary worktree leave uncommitted changes in the primary worktree — intended, commit
27
+ them there. Opt out with `LLM_WIKI_WORKTREE_LOCAL=1`.
28
+ - Expand this file with this repo's own conventions. Keep it short — it loads every session.