@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.
- package/README.md +23 -10
- package/TROUBLESHOOTING.md +9 -0
- package/bin/llm-wiki.js +100 -12
- package/lib/auto-update.js +79 -0
- package/lib/find-doc-root.js +62 -8
- package/lib/init.js +42 -26
- package/lib/kanban-cmd.js +758 -335
- package/lib/kanban-monitor.js +544 -0
- package/lib/kanban-wait.js +215 -0
- package/lib/kanban.js +89 -13
- package/lib/local-today.js +9 -0
- package/lib/skills.js +351 -0
- package/lib/wiki-compile.js +373 -369
- package/lib/wiki-search.js +41 -39
- package/package.json +5 -2
- package/skills/kanban-plan/SKILL.md +21 -4
- package/skills/work-loop/SKILL.md +22 -2
- package/templates/AGENTS.md +6 -1
package/README.md
CHANGED
|
@@ -47,17 +47,26 @@ That's it. The agent does the rest.
|
|
|
47
47
|
| `llm-wiki compile list` | Show raw logs not yet compiled — header date **or** content hash (`compile-state.json`), so same-day appends are caught too |
|
|
48
48
|
| `llm-wiki compile index` | Rebuild `doc/wiki/index.md` (with aliases and answers), regenerate `compile-state.json`, and sync the QMD index. **This is a "compile complete" declaration** — run it after the wiki-compile skill's phases, not instead of them |
|
|
49
49
|
| `llm-wiki lint` | Broken links, **evidence back-matching** (hash refs & `### Error` quotes must exist verbatim in `doc/raw/`), uncompiled concepts, metadata, staleness |
|
|
50
|
-
| `llm-wiki board` / `board report` | Derived kanban view / dashboard (done:abandoned ratio, trend, QA reverts, waiting queue) |
|
|
50
|
+
| `llm-wiki board` / `board report` | Derived kanban view (text only, no flags) / dashboard (done:abandoned ratio, trend, QA reverts, waiting queue) |
|
|
51
51
|
| `llm-wiki board video` | Replay `activity.jsonl` into a board timelapse MP4 (requires the `video/` Remotion project; CPU render, no GPU) |
|
|
52
|
-
| `llm-wiki
|
|
53
|
-
| `llm-wiki
|
|
52
|
+
| `llm-wiki monitor [--port <n>]` | Live read-only board view at `http://127.0.0.1:<n>` (default 4747) — who claimed what and for how long (`zcode · 14분`), claim expiry, REVIEW questions, gates/dependency waits, the terminal pile (종결 적체 — newest done/superseded/abandoned with timestamps), and an activity stream. A **milestone panel** (purpose axis, plan.md 3.8) shows in-progress milestones expanded with their member cards and completed ones collapsed with progress — click a milestone to read its Goal (the plan's 대의) and full member list. Click any card (columns, terminal pile, activity rows) to read the full card file. Polls every 2s (re-renders only on change); the page renders agent-authored text via `textContent` only. Starting one while another monitor already listens is **idempotent** — exit 0, same URL (a port held by anything else fails with guidance). **The CLI stays the only writer** — every non-GET method gets a 405; the server keeps no state |
|
|
53
|
+
| `llm-wiki card new/show/edit` | Create and edit cards — the CLI is the only writer (sentinel-safe sections). `--kind milestone` creates a milestone card (the plan's 대의 lives in its Goal; not pickable, auto-completes when all members terminate); `--milestone "<title>"` attaches a card to one — membership is frontmatter, milestone progress is derived, never stored (plan.md 3.8) |
|
|
54
|
+
| `llm-wiki pick --claim <name> [--card <title>]` | Atomically claim the next eligible card (lock, WIP limit, dependencies, claim expiry). `--card` claims a specific card by title — every gate still applies: a blocked pick prints why and leaves all files untouched, an unknown title fails (exit 1) |
|
|
54
55
|
| `llm-wiki handoff <title> --question "…"` | Park a card for human judgment and release the claim |
|
|
55
|
-
| `llm-wiki done <title> --result "…"` | Complete a card — Result is required |
|
|
56
|
+
| `llm-wiki done <title> --result "…"` | Complete a card — Result is required. Completing the last member auto-completes its milestone |
|
|
56
57
|
| `llm-wiki supersede <title> --by a,b` | Replace a card by children; the parent dissolves into `superseded/` |
|
|
57
58
|
| `llm-wiki abandon <title> --reason "…"` | Discard — reason required, and auto-logged to `doc/raw/` as anti-pattern material |
|
|
58
59
|
| `llm-wiki reopen <title> --why "…"` | QA: revert a fake-done card back to doing |
|
|
60
|
+
| `llm-wiki wait [--for handoff\|done\|any\|stall] [--since <ISO>] [--timeout <s>] [--stall-min <m>] [--json]` | Block until a board event, then exit — **exit codes are the contract: 0 = event (one line to stdout), 2 = timeout (nothing printed), 1 = error**; callers re-arm on 2 and act on 0. `--for` picks the event (`handoff` default; `stall` fires after `--stall-min` minutes of silence, default 20). `--since` (default: now) is checked *before* waiting starts, so events that piled up while the caller was busy are returned immediately. Read-only; timestamp-based (survives wholesale `activity.jsonl` rewrites), skips broken lines, waits for the file to exist, `fs.watch` + 5s poll fallback (junction-safe) |
|
|
59
61
|
|
|
60
|
-
`search`, `lint`, `compile list|index`, `board`, `pick` accept `--json` (a `{schemaVersion: 1, kind: …}` envelope for scripts and skills).
|
|
62
|
+
`search`, `lint`, `compile list|index`, `board report`, `pick` accept `--json` (a `{schemaVersion: 1, kind: …}` envelope for scripts and skills). `wait --json` is different by design: it prints the matched event itself as one JSON line (or a `{action: "stall", …}` line), because callers parse that line directly.
|
|
63
|
+
|
|
64
|
+
Any subcommand accepts `--help`/`-h` anywhere in its args: it prints that command's usage and exits 0 without touching the board — exploring a command must never mutate cards. Every board and card command also rejects unknown flags (`모르는 플래그`), so a typo'd flag fails loudly instead of silently no-oping.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# overnight re-arm loop: wake on the next handoff parked after midnight; silent exit 2 after an hour
|
|
68
|
+
llm-wiki wait --for handoff --since 2026-09-12T00:00:00Z --timeout 3600 --json
|
|
69
|
+
```
|
|
61
70
|
|
|
62
71
|
## What `init` creates
|
|
63
72
|
|
|
@@ -82,15 +91,15 @@ your-repo/
|
|
|
82
91
|
|
|
83
92
|
## Updating
|
|
84
93
|
|
|
85
|
-
The skills, hooks, and scripts copied into your repo are **marker-protected copies
|
|
94
|
+
`npm update` is the only step. The skills, hooks, and scripts copied into your repo are **marker-protected copies**, and the first `llm-wiki` command you run after a version change re-syncs them automatically (it prints one summary line; `llm-wiki init --check` shows the full report, `llm-wiki init` applies manually as before):
|
|
86
95
|
|
|
87
96
|
```bash
|
|
88
|
-
npm update -g llm-wiki #
|
|
89
|
-
llm-wiki init --check # what would change? (ok / stale / user-modified / missing)
|
|
90
|
-
llm-wiki init # apply — stale copies update, your edits survive
|
|
97
|
+
npm update -g @kybird/llm-wiki # then run any llm-wiki command — copies auto-update
|
|
91
98
|
```
|
|
92
99
|
|
|
93
|
-
The
|
|
100
|
+
The version stamp lives per-repo in `~/.llm-wiki/auto-update/` (override with `LLM_WIKI_STATE_DIR`), never inside your repo. Opt out with `"autoUpdate": false` in `llm-wiki.config.json`, or `LLM_WIKI_NO_AUTO_UPDATE=1` for a single run.
|
|
101
|
+
|
|
102
|
+
The contract: a copy keeps its version marker (`skill-version:` in skills, `llm-wiki-template-version:` in hooks and scripts) only while it is unmodified. **To customize a copy, delete its marker line** — updates then treat it as yours and never overwrite it. Repos that keep their own canonical hook can skip the copy entirely: set `"hooksPath": "templates/githooks"` in `llm-wiki.config.json`. The seeded `AGENTS.md` is yours forever — written once at `init`, never updated.
|
|
94
103
|
|
|
95
104
|
## Configuration (optional)
|
|
96
105
|
|
|
@@ -113,6 +122,8 @@ Create `llm-wiki.config.json` in your repo root:
|
|
|
113
122
|
|
|
114
123
|
You can also set `LLM_WIKI_ROOT=/path/to/doc-parent` to point at a `doc/` outside the repo.
|
|
115
124
|
|
|
125
|
+
**Git worktrees:** the board is a project resource, not a branch resource — no matter how many linked worktrees you have, cards, claims and the activity log (`doc/kanban/`) live in one place, the primary worktree's `doc/`. `findDocRoot` resolves a linked worktree to the primary worktree's `doc/` (via `git rev-parse --git-common-dir`). Intended side effect: `pick`/`done` run from a secondary worktree modify files in the primary worktree — those changes stay uncommitted there and are committed there. Set `LLM_WIKI_WORKTREE_LOCAL=1` to restore the old cwd-based (per-worktree) behavior.
|
|
126
|
+
|
|
116
127
|
## How the knowledge flows
|
|
117
128
|
|
|
118
129
|
```
|
|
@@ -164,6 +175,8 @@ git config core.hooksPath templates/githooks
|
|
|
164
175
|
|
|
165
176
|
`llm-wiki.config.json` here declares `"hooksPath": "templates/githooks"`, so `init` never creates a `githooks/` copy — the active hook *is* the canonical template. See [doc/improvement-plan.md](doc/improvement-plan.md) for the roadmap state and [doc/plan.md](doc/plan.md) for the design record.
|
|
166
177
|
|
|
178
|
+
Regression tests (round-trip serialization, duplicate titles, non-card pick, concurrent writes) run on throwaway boards — never touch this repo's `doc/`: `npm test`.
|
|
179
|
+
|
|
167
180
|
## License
|
|
168
181
|
|
|
169
182
|
MIT
|
package/TROUBLESHOOTING.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
>
|
|
5
5
|
> QMD is **optional**. Without it, `llm-wiki search` falls back to grep and the wiki index still rebuilds correctly.
|
|
6
6
|
|
|
7
|
+
## 워크트리마다 보드가 다르게 보인다 (kanban, not QMD)
|
|
8
|
+
|
|
9
|
+
Symptom: two `git worktree`s of the same repo show different boards — a card abandoned in the primary worktree is still live in a linked one, cards created in the primary are invisible in the link, and `doc/kanban/activity.jsonl` conflicts on every merge.
|
|
10
|
+
|
|
11
|
+
That was a defect, fixed in the doc-root resolution: the board is a project resource, not a branch resource. `findDocRoot` (`lib/find-doc-root.js`) now resolves to the **primary worktree's** `doc/` when run inside a linked worktree, so every worktree sees (and writes) the same cards, claims and activity log. Search order: `LLM_WIKI_ROOT` → primary worktree `doc/` (if a git repo and `doc/wiki` exists there) → walk up from cwd → `cwd/doc`.
|
|
12
|
+
|
|
13
|
+
- Intended side effect: `pick`/`done` run from a secondary worktree modify the primary worktree's files. Those changes stay uncommitted in the primary worktree and get committed there. This is by design, not a bug.
|
|
14
|
+
- Opt-out — restore the old per-worktree (cwd-based) behavior: set `LLM_WIKI_WORKTREE_LOCAL=1`.
|
|
15
|
+
|
|
7
16
|
## Runtime CUDA OOM vs build-time compile failure
|
|
8
17
|
|
|
9
18
|
These are two different failure modes:
|
package/bin/llm-wiki.js
CHANGED
|
@@ -7,6 +7,12 @@ const { compile } = require('../lib/wiki-compile');
|
|
|
7
7
|
const { lint } = require('../lib/wiki-lint');
|
|
8
8
|
const { init } = require('../lib/init');
|
|
9
9
|
const kanbanCmd = require('../lib/kanban-cmd');
|
|
10
|
+
const kanbanMonitor = require('../lib/kanban-monitor');
|
|
11
|
+
const kanbanWait = require('../lib/kanban-wait');
|
|
12
|
+
const skills = require('../lib/skills');
|
|
13
|
+
const { findDocRoot } = require('../lib/find-doc-root');
|
|
14
|
+
const { maybeAutoUpdate } = require('../lib/auto-update');
|
|
15
|
+
const { version } = require('../package.json');
|
|
10
16
|
|
|
11
17
|
function printUsage() {
|
|
12
18
|
console.log(`llm-wiki — LLM-friendly knowledge graph + kanban for AI coding agents
|
|
@@ -18,35 +24,95 @@ Usage:
|
|
|
18
24
|
llm-wiki lint Validate wiki integrity (links, metadata, evidence)
|
|
19
25
|
llm-wiki init [--check] Scaffold doc/ + skills/ + hooks (--check: report only)
|
|
20
26
|
|
|
27
|
+
Skills (git channel — prompt edits without npm publish, plan.md 6.2):
|
|
28
|
+
llm-wiki skills add <url> Register a skill repo (owner/repo → GitHub)
|
|
29
|
+
llm-wiki skills remove <url> Unregister
|
|
30
|
+
llm-wiki skills list Show registered sources
|
|
31
|
+
llm-wiki skills sync [--yes] Clone, show diff, install after approval (6.5)
|
|
32
|
+
[--skill <name>] sync one skill only
|
|
33
|
+
|
|
21
34
|
Kanban (cards are files; CLI is the only writer):
|
|
22
|
-
llm-wiki board
|
|
35
|
+
llm-wiki board Derived board view (columns, WIP, queue) — text only, no flags
|
|
23
36
|
llm-wiki board report Dashboard (done:abandoned ratio, trend, reverts)
|
|
24
37
|
llm-wiki board video Timelapse of board activity → MP4 (needs video/ project)
|
|
25
|
-
llm-wiki
|
|
38
|
+
llm-wiki monitor [--port <n>] Live read-only board view at http://127.0.0.1:<n>
|
|
39
|
+
(default 4747) — claims, elapsed, gates, activity;
|
|
40
|
+
the CLI stays the only writer (405 on writes)
|
|
41
|
+
llm-wiki card new "<title>" Create card (--goal, --ac, --depends, --not-before;
|
|
42
|
+
--kind milestone = milestone card, --milestone "<t>"
|
|
43
|
+
attaches to one — plan-level grouping, plan.md 3.8)
|
|
26
44
|
llm-wiki card show <title> Print card file
|
|
27
|
-
llm-wiki card edit <title> Sentinel-safe edits
|
|
28
|
-
|
|
45
|
+
llm-wiki card edit <title> Sentinel-safe edits; unknown/blank flags fail, output
|
|
46
|
+
says what changed (--goal/--plan/--ac/--add-ac/
|
|
47
|
+
--check-ac/--note/--renew-claim/--depends/
|
|
48
|
+
--add-depends/--remove-depends/--milestone)
|
|
49
|
+
--milestone (re)groups an active card under a
|
|
50
|
+
milestone card
|
|
51
|
+
llm-wiki pick --claim <name> [--card <title>]
|
|
52
|
+
Atomically claim the next eligible card (locks, WIP, deps);
|
|
53
|
+
--card names a specific card — gates are never bypassed;
|
|
54
|
+
milestone cards are never picked (plan.md 3.8)
|
|
29
55
|
llm-wiki handoff <title> --question "…" Park for human judgment, release claim
|
|
30
|
-
llm-wiki done <title> --result "…" Complete (Result required)
|
|
56
|
+
llm-wiki done <title> --result "…" Complete (Result required). Completing the
|
|
57
|
+
last member auto-completes its milestone (3.8)
|
|
31
58
|
llm-wiki supersede <title> --by a,b Replace by children (parent dissolves)
|
|
32
59
|
llm-wiki abandon <title> --reason "…" Discard (reason required, never deleted)
|
|
33
60
|
llm-wiki reopen <title> --why "…" QA: revert a fake-done card to doing
|
|
34
61
|
llm-wiki resume <title> [--note "…"] Return a review (parked) card to todo
|
|
62
|
+
llm-wiki wait [--for <필터>] [--since <ISO ts>] [--timeout <초>] [--stall-min <분>] [--json]
|
|
63
|
+
Block until a board event, then exit. 종료 코드가 계약:
|
|
64
|
+
0 = 이벤트(stdout 한 줄), 2 = 타임아웃(출력 없음 — 조용히
|
|
65
|
+
재무장), 1 = 오류. 필터: handoff(기본) | done | any |
|
|
66
|
+
stall(--stall-min 분 무활동 후 0, 기본 20). --since 생략
|
|
67
|
+
시 지금 — 이후의 기존 이벤트는 대기 전에 먼저 검사한다
|
|
35
68
|
|
|
36
69
|
Optional:
|
|
37
70
|
npm i @tobilu/qmd Enable semantic search (falls back to grep if absent)
|
|
71
|
+
npm update -g @kybird/llm-wiki Copied skills/hooks/scripts auto-update on the next
|
|
72
|
+
command ("autoUpdate": false in llm-wiki.config.json,
|
|
73
|
+
or LLM_WIKI_NO_AUTO_UPDATE=1, to opt out)
|
|
38
74
|
--json Machine-readable output: {schemaVersion: 1, kind: ...}
|
|
39
|
-
(search, lint, compile list|index)
|
|
75
|
+
(search, lint, compile list|index, board report, pick)
|
|
40
76
|
LLM_WIKI_ROOT=/path Override doc/ root location
|
|
41
77
|
llm-wiki.config.json { "projectName": "...", "collections": {...},
|
|
42
|
-
"hooksPath": "templates/githooks"
|
|
78
|
+
"hooksPath": "templates/githooks",
|
|
79
|
+
"skills": { "sources": ["<git-url>"], "enabled": ["<name>"] } }`);
|
|
43
80
|
}
|
|
44
81
|
|
|
45
82
|
const [, , subcommand, ...rest] = process.argv;
|
|
46
|
-
// --json
|
|
83
|
+
// --json은 어느 위치에 와도 플래그로 뽑아낸다 (검색어 문자열에서 제외). board는
|
|
84
|
+
// --json/--html을 받지 않는다 — 추출된 사실을 dispatch 너머 명령에 넘겨 명시적으로
|
|
85
|
+
// 실패시킨다(전역 선추출이 validateFlags를 우회하게 두면 조용한 no-op 성공이 된다).
|
|
47
86
|
const jsonRequested = rest.includes('--json');
|
|
48
|
-
const
|
|
49
|
-
|
|
87
|
+
const args = rest.filter(a => a !== '--json');
|
|
88
|
+
|
|
89
|
+
// 가드 — 탐색용 호출이 보드를 건드리지 못하게(2026-09-11·12 사고). 부작용 있는
|
|
90
|
+
// 서브커맨드를 확인하려고 `pick --help`를 쳤다가 실제로 카드를 집는 일을 원천
|
|
91
|
+
// 차단한다: 알려진 서브커맨드의 args 어디에 --help/-h가 있어도 그 명령의 사용법만
|
|
92
|
+
// 찍고 나간다. 종료 코드 0 — --help는 오류가 아니고, 카드 파일과 activity 로그는
|
|
93
|
+
// 한 글자도 바뀌지 않는다. 사용법 문자열은 각 명령의 fail()이 쓰는 것과 같은 곳
|
|
94
|
+
// (kanban-cmd의 USAGE)에서 온다. 전용 문자열이 없는 읽기 전용 명령(search 등)은
|
|
95
|
+
// 전체 사용법으로 대신한다. return으로 자연 종료한다 — Windows 파이프 stdout은
|
|
96
|
+
// 비동기라 process.exit은 마지막 줄을 지울 수 있다(kanban-wait 교훈).
|
|
97
|
+
const KNOWN_SUBCOMMANDS = ['search', 'compile', 'lint', 'init', 'skills', 'board', 'monitor', 'card',
|
|
98
|
+
'pick', 'handoff', 'done', 'supersede', 'abandon', 'reopen', 'resume', 'wait'];
|
|
99
|
+
if (KNOWN_SUBCOMMANDS.includes(subcommand) && (args.includes('--help') || args.includes('-h'))) {
|
|
100
|
+
const usageKey = subcommand === 'card' && ['new', 'show', 'edit'].includes(args[0]) ? `card ${args[0]}` : subcommand;
|
|
101
|
+
const usage = kanbanCmd.USAGE[usageKey];
|
|
102
|
+
if (usage) console.log(usage);
|
|
103
|
+
else printUsage();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// npm 업데이트 자동 반영(README "Updating") — 리포를 실제로 쓰는 명령 앞에서만.
|
|
108
|
+
// init은 자체 동기화 흐름이 있고, 도움말·버전·오타 명령은 리포를 건드릴 이유가 없다.
|
|
109
|
+
// wait도 빠진다 — 백그라운드 관측 명령이라 동기화를 몰고 올 이유가 없고, 무엇보다
|
|
110
|
+
// stdout이 "이벤트 한 줄 or 침묵" 계약이라 auto-update 배너가 그 계약을 깬다.
|
|
111
|
+
// 실패는 maybeAutoUpdate 안에서 삼켜진다 — 갱신 실패가 명령을 막지 않는다.
|
|
112
|
+
if (['search', 'compile', 'lint', 'skills', 'board', 'card', 'pick', 'handoff',
|
|
113
|
+
'done', 'supersede', 'abandon', 'reopen', 'resume'].includes(subcommand)) {
|
|
114
|
+
maybeAutoUpdate();
|
|
115
|
+
}
|
|
50
116
|
|
|
51
117
|
switch (subcommand) {
|
|
52
118
|
case 'search':
|
|
@@ -61,10 +127,25 @@ switch (subcommand) {
|
|
|
61
127
|
case 'init':
|
|
62
128
|
init({ check: args.includes('--check') });
|
|
63
129
|
break;
|
|
130
|
+
case 'skills':
|
|
131
|
+
// sync는 diff 제시 후 승인 프롬프트를 위해 async다.
|
|
132
|
+
skills.dispatch(args, findDocRoot()).catch(e => {
|
|
133
|
+
console.error(e.message);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
64
137
|
case 'board':
|
|
65
138
|
if (args[0] === 'report') kanbanCmd.boardReport({ json: jsonRequested });
|
|
66
|
-
else if (args[0] === 'video') kanbanCmd.boardVideo({ rest: args });
|
|
67
|
-
else kanbanCmd.boardView({ rest: args, json: jsonRequested
|
|
139
|
+
else if (args[0] === 'video') kanbanCmd.boardVideo({ rest: args, json: jsonRequested });
|
|
140
|
+
else kanbanCmd.boardView({ rest: args, json: jsonRequested });
|
|
141
|
+
break;
|
|
142
|
+
case 'monitor':
|
|
143
|
+
// auto-update 트리거 목록에 없다 — stdout 첫 줄이 URL 계약이라 배너가 깨면
|
|
144
|
+
// 안 된다(wait 제외와 같은 이유). monitor는 async(멱등 재사용 검사)다.
|
|
145
|
+
kanbanMonitor.monitor({ rest: args, json: jsonRequested }).catch(e => {
|
|
146
|
+
console.error(`✗ ${e.message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
68
149
|
break;
|
|
69
150
|
case 'card':
|
|
70
151
|
kanbanCmd.dispatchCard(args);
|
|
@@ -90,11 +171,18 @@ switch (subcommand) {
|
|
|
90
171
|
case 'resume':
|
|
91
172
|
kanbanCmd.resume({ rest: args });
|
|
92
173
|
break;
|
|
174
|
+
case 'wait':
|
|
175
|
+
kanbanWait.wait({ rest: args, json: jsonRequested });
|
|
176
|
+
break;
|
|
93
177
|
case '--help':
|
|
94
178
|
case '-h':
|
|
95
179
|
case undefined:
|
|
96
180
|
printUsage();
|
|
97
181
|
break;
|
|
182
|
+
case '--version':
|
|
183
|
+
case '-v':
|
|
184
|
+
console.log(version);
|
|
185
|
+
break;
|
|
98
186
|
default:
|
|
99
187
|
console.error(`Unknown command: ${subcommand}`);
|
|
100
188
|
printUsage();
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// npm 업데이트 자동 반영 (README "Updating").
|
|
2
|
+
// 설치된 패키지 버전과 이 레포가 마지막으로 동기화한 버전이 다르면, init과 같은
|
|
3
|
+
// 마커 인식 동기화(syncCopies)를 자동으로 돈다 — 사용자는 npm update 만 하면 되고
|
|
4
|
+
// 사본(skills/·scripts/·githooks)은 다음 어떤 명령에서든 따라온다.
|
|
5
|
+
//
|
|
6
|
+
// 버전 스탬프는 사용자 홈(~/.llm-wiki/auto-update/<레포경로해시>.version)에 둔다 —
|
|
7
|
+
// 소비 리포의 커밋 트리를 오염시키지 않고, git 없는 디렉터리에서도 동작한다.
|
|
8
|
+
// LLM_WIKI_STATE_DIR로 위치를 바꿀 수 있다(테스트 밀폐용).
|
|
9
|
+
// 어떤 실패든 조용히 넘어간다 — 갱신 실패가 명령 자체를 막으면 안 된다.
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { findDocRoot, loadConfig } = require('./find-doc-root');
|
|
15
|
+
const { syncCopies } = require('./init');
|
|
16
|
+
const { version: PKG_VERSION } = require('../package.json');
|
|
17
|
+
|
|
18
|
+
function stateDir() {
|
|
19
|
+
return process.env.LLM_WIKI_STATE_DIR || path.join(os.homedir(), '.llm-wiki');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stampPath(repoRoot) {
|
|
23
|
+
// resolve로 정규화해 해시 — 슬래시/백슬래시 모양 차이가 키를 갈라놓지 않게.
|
|
24
|
+
const key = crypto.createHash('md5').update(path.resolve(repoRoot)).digest('hex').slice(0, 16);
|
|
25
|
+
return path.join(stateDir(), 'auto-update', `${key}.version`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readStamp(p) {
|
|
29
|
+
try {
|
|
30
|
+
return fs.readFileSync(p, 'utf8').trim();
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 명령 디스패치 직전에 부른다. docRoot를 (있다면) findDocRoot로 스스로 정한다.
|
|
37
|
+
function maybeAutoUpdate() {
|
|
38
|
+
try {
|
|
39
|
+
const docRoot = findDocRoot();
|
|
40
|
+
const root = path.dirname(path.resolve(docRoot));
|
|
41
|
+
|
|
42
|
+
// init된 흔적이 없는 디렉터리에서는 아무것도 쓰지 않는다 — 모르는 리포 오염 금지.
|
|
43
|
+
// (findDocRoot의 cwd/doc 폴백이 낯선 디렉터리를 돌려줄 수 있다.)
|
|
44
|
+
const initMark =
|
|
45
|
+
fs.existsSync(path.join(root, 'doc', 'kanban')) ||
|
|
46
|
+
fs.existsSync(path.join(root, 'doc', 'wiki'));
|
|
47
|
+
if (!initMark) return;
|
|
48
|
+
|
|
49
|
+
const cfg = loadConfig(path.join(root, 'doc'));
|
|
50
|
+
if (cfg.autoUpdate === false) return; // 옵트아웃 — init 재실행으로만 갱신
|
|
51
|
+
if (process.env.LLM_WIKI_NO_AUTO_UPDATE === '1') return; // 일회성 끄기
|
|
52
|
+
|
|
53
|
+
const sp = stampPath(root);
|
|
54
|
+
if (readStamp(sp) === PKG_VERSION) return; // 이 버전으로 이미 동기화됨
|
|
55
|
+
|
|
56
|
+
const copies = syncCopies(root, { apply: true, config: cfg });
|
|
57
|
+
const actions = [...copies.skills, ...copies.scripts, ...copies.hooks];
|
|
58
|
+
const changed = actions.filter(a => a.status === 'updated' || a.status === 'copied');
|
|
59
|
+
const userModified = actions.filter(a => a.status === 'user-modified').length;
|
|
60
|
+
|
|
61
|
+
fs.mkdirSync(path.dirname(sp), { recursive: true });
|
|
62
|
+
fs.writeFileSync(sp, PKG_VERSION);
|
|
63
|
+
|
|
64
|
+
// 조용한 no-op 금지(계약): 뭘 바꿨는지 한 줄로 말한다. 바뀐 게 없으면 침묵.
|
|
65
|
+
if (changed.length > 0) {
|
|
66
|
+
const updated = changed.filter(a => a.status === 'updated').length;
|
|
67
|
+
const copied = changed.length - updated;
|
|
68
|
+
const skipped = userModified > 0 ? ` · 사용자 수정본 건너뜀 ${userModified}` : '';
|
|
69
|
+
console.log(
|
|
70
|
+
`llm-wiki v${PKG_VERSION}: npm 업데이트 자동 반영 — 사본 갱신 updated ${updated} · copied ${copied}${skipped}` +
|
|
71
|
+
` (상세: llm-wiki init --check)`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// 자동 갱신은 부가 기능이다 — 어떤 실패든 명령을 막지 않는다.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { maybeAutoUpdate, stampPath };
|
package/lib/find-doc-root.js
CHANGED
|
@@ -1,13 +1,55 @@
|
|
|
1
|
-
//
|
|
1
|
+
// doc/ 루트 자동 탐색.
|
|
2
2
|
// llm-wiki는 어느 리포에서 실행되든 동작해야 하므로, 스크립트 자신의 위치(__dirname)
|
|
3
|
-
// 에 의존하지
|
|
3
|
+
// 에 의존하지 않는다. 보드(카드·클레임·활동 로그)는 프로젝트 자원이지 브랜치 자원이
|
|
4
|
+
// 아니므로 git 링크 워크트리에서 실행돼도 주 워크트리의 doc/을 향한다(2026-09-12
|
|
5
|
+
// sugarScan 실측 — 워크트리마다 보드가 갈라져 폐기 카드가 부 워크트리에서 되살아났다).
|
|
4
6
|
//
|
|
5
7
|
// 탐색 우선순위:
|
|
6
|
-
// 1. LLM_WIKI_ROOT 환경변수 (명시적 오버라이드)
|
|
7
|
-
// 2.
|
|
8
|
-
// 3. cwd/
|
|
8
|
+
// 1. LLM_WIKI_ROOT 환경변수 (명시적 오버라이드 — git을 부르지 않는다)
|
|
9
|
+
// 2. git 저장소면 주 워크트리의 doc/ (doc/wiki가 있을 때; LLM_WIKI_WORKTREE_LOCAL=1로 끈다)
|
|
10
|
+
// 3. cwd에서 위로 올라가며 doc/wiki 디렉토리가 있는 첫 조상 (git 아닌 곳 폴백)
|
|
11
|
+
// 4. cwd/doc (init 전이라 없어도 호출 가능하도록 폴백)
|
|
9
12
|
const fs = require('fs');
|
|
10
13
|
const path = require('path');
|
|
14
|
+
const { spawnSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
// 2단계 탐색 결과 캐시 — findDocRoot는 명령마다 여러 번 불리므로 git 자식 프로세스는
|
|
17
|
+
// 시작 디렉터리당 프로세스에서 한 번만 돌린다 (value가 null이어도 캐시한다).
|
|
18
|
+
let primaryWorktreeDocCache = { startDir: null, doc: null };
|
|
19
|
+
|
|
20
|
+
function gitOutput(args, cwd) {
|
|
21
|
+
try {
|
|
22
|
+
const r = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 5000 });
|
|
23
|
+
if (r.error || r.status !== 0) return null; // git 없음·저장소 아님·타임아웃
|
|
24
|
+
return (r.stdout || '').trim() || null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 주 워크트리의 doc/ 경로. 못 찾으면(비저장소, init 전, git 실패) null → 상위 탐색으로.
|
|
31
|
+
function primaryWorktreeDoc(startDir) {
|
|
32
|
+
const key = path.resolve(startDir);
|
|
33
|
+
if (primaryWorktreeDocCache.startDir === key) return primaryWorktreeDocCache.doc;
|
|
34
|
+
let doc = null;
|
|
35
|
+
// --path-format=absolute는 git 2.31+ 옵션이다 — 실패하면 옛 git으로 한 번 더.
|
|
36
|
+
let commonDir = gitOutput(['rev-parse', '--path-format=absolute', '--git-common-dir'], key);
|
|
37
|
+
if (!commonDir) {
|
|
38
|
+
commonDir = gitOutput(['rev-parse', '--git-common-dir'], key);
|
|
39
|
+
}
|
|
40
|
+
if (commonDir) {
|
|
41
|
+
// 상대 경로로 나오면 cwd 기준으로 절대화. dirname(<주 워크트리>/.git) = 저장소 루트.
|
|
42
|
+
const primaryRoot = path.dirname(path.resolve(key, commonDir));
|
|
43
|
+
const wiki = path.join(primaryRoot, 'doc', 'wiki');
|
|
44
|
+
try {
|
|
45
|
+
if (fs.statSync(wiki).isDirectory()) doc = path.join(primaryRoot, 'doc');
|
|
46
|
+
} catch {
|
|
47
|
+
// 주 워크트리에 doc/wiki 없음 = init 전 — 조용히 상위 탐색으로
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
primaryWorktreeDocCache = { startDir: key, doc };
|
|
51
|
+
return doc;
|
|
52
|
+
}
|
|
11
53
|
|
|
12
54
|
function findDocRoot(startDir) {
|
|
13
55
|
// 1) 환경변수 오버라이드
|
|
@@ -15,8 +57,17 @@ function findDocRoot(startDir) {
|
|
|
15
57
|
return path.resolve(process.env.LLM_WIKI_ROOT);
|
|
16
58
|
}
|
|
17
59
|
|
|
18
|
-
|
|
19
|
-
|
|
60
|
+
const start = startDir || process.cwd();
|
|
61
|
+
|
|
62
|
+
// 2) 주 워크트리의 doc/ — 링크 워크트리의 브랜치 사본이 아니라 프로젝트 정본.
|
|
63
|
+
// LLM_WIKI_WORKTREE_LOCAL=1이면 종전 동작(cwd 기준)으로 되돌린다.
|
|
64
|
+
if (process.env.LLM_WIKI_WORKTREE_LOCAL !== '1') {
|
|
65
|
+
const primaryDoc = primaryWorktreeDoc(start);
|
|
66
|
+
if (primaryDoc) return primaryDoc;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3) cwd에서 위로 올라가며 doc/wiki 탐색
|
|
70
|
+
let dir = path.resolve(start);
|
|
20
71
|
for (let i = 0; i < 20; i++) {
|
|
21
72
|
const candidate = path.join(dir, 'doc', 'wiki');
|
|
22
73
|
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
@@ -27,7 +78,7 @@ function findDocRoot(startDir) {
|
|
|
27
78
|
dir = parent;
|
|
28
79
|
}
|
|
29
80
|
|
|
30
|
-
//
|
|
81
|
+
// 4) 폴백: cwd/doc (init 전 상태에서도 호출 허용)
|
|
31
82
|
return path.join(process.cwd(), 'doc');
|
|
32
83
|
}
|
|
33
84
|
|
|
@@ -63,6 +114,8 @@ function loadConfig(docRoot) {
|
|
|
63
114
|
projectName: null, // null이면 index.md 헤더에 프로젝트명 생략
|
|
64
115
|
collections: defaultCollectionNames(docRoot),
|
|
65
116
|
hooksPath: null,
|
|
117
|
+
// npm 업데이트 자동 반영(README "Updating") — false면 init 재실행으로만 갱신.
|
|
118
|
+
autoUpdate: true,
|
|
66
119
|
};
|
|
67
120
|
|
|
68
121
|
const candidates = [
|
|
@@ -81,6 +134,7 @@ function loadConfig(docRoot) {
|
|
|
81
134
|
raw: (user.collections && user.collections.raw) || defaults.collections.raw,
|
|
82
135
|
},
|
|
83
136
|
hooksPath: user.hooksPath !== undefined ? user.hooksPath : defaults.hooksPath,
|
|
137
|
+
autoUpdate: user.autoUpdate !== undefined ? user.autoUpdate : defaults.autoUpdate,
|
|
84
138
|
};
|
|
85
139
|
} catch (e) {
|
|
86
140
|
// 깨진 config는 무시하고 기본값 사용 (사용자에게 에러 띄우지 않음)
|
package/lib/init.js
CHANGED
|
@@ -137,8 +137,39 @@ function exists(p) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// skills/·scripts/·githooks 사본을 마커 규칙대로 동기화한다 — init 본체와 자동 갱신
|
|
141
|
+
// (lib/auto-update.js)이 같은 쓰기를 공유한다. 따로 두면 어느 한쪽만 새로워진다.
|
|
142
|
+
// config는 hooksPath 판정에 필요 — 없으면 여기서 읽는다.
|
|
143
|
+
function syncCopies(cwd, { apply = false, config } = {}) {
|
|
144
|
+
const cfg = config || loadConfig(path.join(cwd, 'doc'));
|
|
145
|
+
const out = { skills: [], scripts: [], hooks: [], hooksSkipped: null };
|
|
146
|
+
|
|
147
|
+
out.skills = syncSkills(
|
|
148
|
+
path.join(PKG_ROOT, 'skills'),
|
|
149
|
+
SKILL_TARGETS.map(t => path.join(cwd, t)),
|
|
150
|
+
{ apply }
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const scriptsDest = path.join(cwd, 'scripts');
|
|
154
|
+
if (apply) fs.mkdirSync(scriptsDest, { recursive: true });
|
|
155
|
+
out.scripts = syncMarkedFiles(path.join(PKG_ROOT, 'templates', 'scripts'), [scriptsDest], TEMPLATE_VERSION_RE, { apply });
|
|
156
|
+
|
|
157
|
+
if (cfg.hooksPath) {
|
|
158
|
+
out.hooksSkipped = cfg.hooksPath;
|
|
159
|
+
} else {
|
|
160
|
+
const hooksDest = path.join(cwd, 'githooks');
|
|
161
|
+
if (apply) fs.mkdirSync(hooksDest, { recursive: true });
|
|
162
|
+
out.hooks = syncMarkedFiles(path.join(PKG_ROOT, 'templates', 'githooks'), [hooksDest], TEMPLATE_VERSION_RE, { apply });
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
140
167
|
function init(options = {}) {
|
|
141
|
-
|
|
168
|
+
// LLM_WIKI_ROOT는 doc/ 루트 자체를 가리킨다 — 오버라이드가 있으면 그 부모를
|
|
169
|
+
// 레포 루트로 삼아 거기에 스캐폴드한다. 무시하면 init은 cwd/doc에 골격을 놓고
|
|
170
|
+
// 다른 명령은 오버라이드 루트를 읽어 "doc/kanban이 없다 → init을 실행하라"의
|
|
171
|
+
// 안내 무한루프가 된다(2026-09-09 리뷰 5-2).
|
|
172
|
+
const cwd = process.env.LLM_WIKI_ROOT ? path.dirname(path.resolve(process.env.LLM_WIKI_ROOT)) : process.cwd();
|
|
142
173
|
|
|
143
174
|
// --check: 아무것도 쓰지 않고 레포 복사본 vs 패키지 정본의 신선함만 보고 (개선계획 1-7).
|
|
144
175
|
if (options.check) {
|
|
@@ -189,36 +220,21 @@ function init(options = {}) {
|
|
|
189
220
|
console.log('✓ AGENTS.md already exists, untouched.');
|
|
190
221
|
}
|
|
191
222
|
|
|
192
|
-
// 2. skills
|
|
193
|
-
//
|
|
194
|
-
const
|
|
195
|
-
const
|
|
223
|
+
// 2~4. skills/·scripts/·githooks 사본 동기화 — 마커 규칙(살아있으면 갱신, 지웠으면
|
|
224
|
+
// 사용자 수정본으로 건너뜀)은 syncCopies가 지킨다. init과 자동 갱신이 공유.
|
|
225
|
+
const cfg = loadConfig(docDir);
|
|
226
|
+
const copies = syncCopies(cwd, { apply: true, config: cfg });
|
|
196
227
|
for (const target of SKILL_TARGETS) {
|
|
197
228
|
console.log(`✓ Synced skills/ → ${target}/`);
|
|
198
229
|
}
|
|
199
|
-
printSkillSyncReport(
|
|
200
|
-
|
|
201
|
-
// 3. scripts/ 동기화 스크립트 — 스킬과 같은 마커 규칙으로 동기화(2026-08-29 확장).
|
|
202
|
-
// 마커를 지운 파일(사용자 수정)은 건너뛴다 — init이 훅 보조 스크립트를 덮어쓰지 않는다.
|
|
203
|
-
const scriptsSrc = path.join(PKG_ROOT, 'templates', 'scripts');
|
|
204
|
-
const scriptsDest = path.join(cwd, 'scripts');
|
|
205
|
-
fs.mkdirSync(scriptsDest, { recursive: true });
|
|
206
|
-
const scriptsActions = syncMarkedFiles(scriptsSrc, [scriptsDest], TEMPLATE_VERSION_RE, { apply: true });
|
|
230
|
+
printSkillSyncReport(copies.skills, cwd);
|
|
207
231
|
console.log('✓ Synced templates/scripts → scripts/');
|
|
208
|
-
printSkillSyncReport(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// (이 레포처럼 templates/를 정본으로 직접 쓰는 경우) 사본을 만들지 않는다.
|
|
212
|
-
const cfg = loadConfig(docDir);
|
|
213
|
-
if (cfg.hooksPath) {
|
|
214
|
-
console.log(`✓ hooks: 레포 선언 경로 ${cfg.hooksPath} 를 그대로 쓴다 (githooks/ 사본 없음).`);
|
|
232
|
+
printSkillSyncReport(copies.scripts, cwd);
|
|
233
|
+
if (copies.hooksSkipped) {
|
|
234
|
+
console.log(`✓ hooks: 레포 선언 경로 ${copies.hooksSkipped} 를 그대로 쓴다 (githooks/ 사본 없음).`);
|
|
215
235
|
} else {
|
|
216
|
-
const hooksSrc = path.join(PKG_ROOT, 'templates', 'githooks');
|
|
217
|
-
const hooksDest = path.join(cwd, 'githooks');
|
|
218
|
-
fs.mkdirSync(hooksDest, { recursive: true });
|
|
219
|
-
const hooksActions = syncMarkedFiles(hooksSrc, [hooksDest], TEMPLATE_VERSION_RE, { apply: true });
|
|
220
236
|
console.log('✓ Synced templates/githooks → githooks/');
|
|
221
|
-
printSkillSyncReport(
|
|
237
|
+
printSkillSyncReport(copies.hooks, cwd);
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
// 5. QMD 컬렉션 이름 충돌 검사 (리포 폴더명 기반 기본값이라도, 여러 리포명이 sanitize 후
|
|
@@ -243,4 +259,4 @@ function init(options = {}) {
|
|
|
243
259
|
console.log('Done. AI agents will now use wiki-search/log/compile/lint + work-loop/board.');
|
|
244
260
|
}
|
|
245
261
|
|
|
246
|
-
module.exports = { init };
|
|
262
|
+
module.exports = { init, syncCopies };
|