@kybird/llm-wiki 0.2.2 → 0.4.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.2",
3
+ "version": "0.4.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-board-flags.test.js test/kanban-milestone.test.js test/kanban-monitor.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: kanban-plan
3
- description: Planning loop — turn a plan into board cards the work-loop can consume. Cards are written only via the CLI; decompose with divergence guards, gate what isn't ready.
4
- skill-version: 2
3
+ description: Planning loop — turn a plan into board cards the work-loop can consume. Cards are written only via the CLI; decompose with divergence guards, gate what isn't ready, group multi-card plans under a milestone.
4
+ skill-version: 4
5
5
  ---
6
6
  # When to use
7
7
 
@@ -15,7 +15,7 @@ skill-version: 2
15
15
  - 기획(this skill) = **분해**: a plan becomes cards.
16
16
  - 개발(`work-loop`) = **해소**: pick → done.
17
17
  - QA = **수렴 강제**: fake dones get reverted; `board report` shows the ratio.
18
- - The person watches progress through `llm-wiki board` / `board report` / `board --html`.
18
+ - The person watches progress through `llm-wiki board` / `board report` / `llm-wiki monitor`.
19
19
  Plan so that this one screen is enough — 리뷰가 카드 수에 비례하면 무인의 의미가 없다.
20
20
 
21
21
  # Before creating cards
@@ -26,6 +26,23 @@ skill-version: 2
26
26
  it (`supersede`); never spawn a near-duplicate. Titles are identifiers (plan.md 3.5):
27
27
  no numbers, no versions in titles.
28
28
 
29
+ # Milestones — 계획 단위 소속 (plan.md 3.8)
30
+
31
+ A plan that decomposes into **2+ cards gets one milestone card** — the board's purpose
32
+ axis. One-card work needs no milestone (과잉이다).
33
+
34
+ 1. `card new "<계획 제목>" --kind milestone --goal "<대의 한 문장>"` — the Goal is why
35
+ the plan exists; the morning human reads it above the terminal stream.
36
+ 2. Every member card carries `--milestone "<계획 제목>"` (or `card edit --milestone`
37
+ later — active cards only; terminal membership is history).
38
+ 3. The milestone is never picked and never closed by hand: `done`/`supersede`/`abandon`
39
+ of the last member auto-completes it (review-parked milestones only report).
40
+ Milestone progress is **derived**, never stored — don't manage its state.
41
+
42
+ Rules: milestones stay **flat** (a milestone never belongs to another milestone — CLI
43
+ rejects it). Gates don't propagate — each member carries its own `--not-before`.
44
+ Abandoning a milestone requires its members to be terminal first (CLI enforces, lists them).
45
+
29
46
  # Decomposition rules (divergence guards, plan.md 2.3)
30
47
 
31
48
  - One card = one context = one commit. If you cannot say what the commit would be, it is
@@ -33,7 +50,7 @@ skill-version: 2
33
50
  - Children must be **strictly smaller** than the parent. When children replace a parent,
34
51
  `supersede` it — the parent dissolves, it is not marked done.
35
52
  - Depth ≤ 3 (plan → subtask → task). Deeper than that means you are writing the work,
36
- not planning it.
53
+ not planning it. (The milestone sits **above** this ladder as grouping, not depth.)
37
54
  - Do not board what is not ready to start:
38
55
  - Time condition → `card new "<t>" --not-before 2026-09-05`. `pick` skips it until then.
39
56
  - Observational condition → create it, then `handoff <t> --question "조건: …"`. It waits
@@ -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: 7
5
5
  ---
6
6
  # When to use
7
7
 
@@ -18,6 +18,14 @@ skill-version: 4
18
18
  3. Never build or assume a process supervisor. The loop lives in the session; the board
19
19
  lives in files.
20
20
 
21
+ # Loop start — bring the monitor up (사람의 실시간 관측)
22
+
23
+ 루프를 시작할 때 `llm-wiki monitor`를 백그라운드로 띄운다 — 밤중에 사람이 들어와도
24
+ 클레임·진행·마일스톤이 살아 있는 보드를 본다. **멱등이다**: 이미 떠 있으면 exit 0으로
25
+ 같은 URL을 되뇌므로 그대로 쓰고, 백그라운드 실행이 안 되는 환경이면 건너뛴다 —
26
+ 모니터는 관측이지 의존성이 아니다. 정지 규칙에서도 모니터는 끄지 않는다(아침의
27
+ 사람이 본다) — 읽기 전용이라 남겨두는 비용이 없다.
28
+
21
29
  # Loop graph
22
30
 
23
31
  ```
@@ -27,7 +35,7 @@ pick → work → 판정 ─ done / handoff / abandon / supersede
27
35
  집을 카드 없음 → review(대기) 큐 점검 → 전부 대기면 질문을 모아 정지 (반스래시)
28
36
  ```
29
37
 
30
- # Rules (all six are load-bearing)
38
+ # Rules (all seven are load-bearing)
31
39
 
32
40
  0. **Condition-gated cards.** Some cards must not start yet:
33
41
  - `not_before: YYYY-MM-DD` in frontmatter (future date) — `pick` skips them
@@ -61,6 +69,10 @@ pick → work → 판정 ─ done / handoff / abandon / supersede
61
69
  (보고서와 함께 세션 타임랩스 영상을 남긴다 — 아침의 사람이 40초로 밤을 본다),
62
70
  leave the questions in one place, and STOP. Do not invent new cards to look
63
71
  productive. 밤새 카드가 300장이 되는 것이 이 시스템이 죽는 방식이다 (plan.md 2.3).
72
+ Milestone cards are **never pickable** (they are grouping, not work — plan.md 3.8):
73
+ if the No-pickable detail lists them, that is normal — don't `done` or `abandon` a
74
+ milestone by hand; it auto-completes when its last member terminates (a `done` that
75
+ prints `◉ milestone 완료` closed it for you).
64
76
 
65
77
  5. **Search before work.** `llm-wiki search "<keywords>"` before starting a card. If the
66
78
  wall you are about to hit already has an abandoned card or an anti-pattern page, skip
@@ -70,6 +82,14 @@ pick → work → 판정 ─ done / handoff / abandon / supersede
70
82
  On a long card, `llm-wiki card edit <제목> --renew-claim` before the timeout, or
71
83
  another loop instance will reclaim the card under you.
72
84
 
85
+ 7. **Worktrees share one board.** The board is a project resource, not a branch resource:
86
+ from a linked worktree, every `pick`/`done`/`card` writes the **primary worktree's**
87
+ `doc/kanban/`. If this loop runs in a linked worktree, its card changes appear as
88
+ uncommitted changes in the primary worktree — leave them there (the primary's next
89
+ commit picks them up); do not chase them into this worktree's commits, and do not
90
+ commit in the primary from here. `LLM_WIKI_WORKTREE_LOCAL=1` restores per-worktree
91
+ boards.
92
+
73
93
  # Splitting (supersede) — divergence guard
74
94
 
75
95
  Supersede only when each child is **strictly smaller** than the parent in context needed.
@@ -13,12 +13,17 @@ Files are the source of truth. Kanban card files are written ONLY via the CLI
13
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
14
  | When raw logs have accumulated | `wiki-compile` skill → promote to `doc/wiki/` pages, then `llm-wiki compile index` |
15
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>"` |
16
+ | Planning work (person present) | `kanban-plan` skill → cards via `llm-wiki card new "<title>"`. A plan that splits into 2+ cards gets **one milestone card** (`--kind milestone`, members carry `--milestone`); milestones are never picked and auto-complete when all members terminate |
17
17
  | Unattended execution | `work-loop` skill → `llm-wiki pick --claim <name>`, park judgment calls with `handoff` |
18
+ | Watching the board (human) | `llm-wiki monitor` — read-only live view (claims, elapsed, gates, terminal pile, milestones) at `http://127.0.0.1:4747` |
18
19
 
19
20
  ## Rules
20
21
 
21
22
  - Quote error messages **character-for-character** in logs and wiki pages — `llm-wiki lint`
22
23
  back-checks every quote and `hash:` against `doc/raw/`.
23
24
  - Follow `status: deprecated` → `superseded_by` when reading wiki pages.
25
+ - The board is a **project** resource, not a branch resource: from any git linked worktree,
26
+ `llm-wiki` reads and writes the **primary worktree's** `doc/kanban/`. `pick`/`done` run in
27
+ a secondary worktree leave uncommitted changes in the primary worktree — intended, commit
28
+ them there. Opt out with `LLM_WIKI_WORKTREE_LOCAL=1`.
24
29
  - Expand this file with this repo's own conventions. Keep it short — it loads every session.