llm-wiki-kit 0.2.10 → 0.2.11

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 CHANGED
@@ -89,11 +89,11 @@ The installed hooks:
89
89
  - inject functional compact context at session start, instructions loaded, and prompt submit. The hook still uses `wiki/memory.md`, `wiki/index.md`, relevant wiki search results, maintenance signals, update status, and any compact recovery packet; it formats only the useful parts so user-visible hook context does not look like a raw debug dump.
90
90
  - remove Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` surfaces at session start so `llm-wiki/` remains the active wiki implementation
91
91
  - record small redacted raw event envelopes and per-turn state
92
- - capture decision points, debugging findings, changed files, and verification notes
93
- - before compaction, classify the current turn and save a redacted checkpoint for meaningful or durable work; durable candidates also get a maintenance queue item
92
+ - capture meaningful work and structured decision points, including tool evidence, changed files, and verification notes
93
+ - before compaction, classify the current turn and save a redacted checkpoint only for meaningful work, structured decisions, or explicit durable requests; explicit durable candidates also get a maintenance queue item when no durable wiki update is detected
94
94
  - after compaction, store the redacted compact summary only; if pre-compact preservation failed, prepare a recovery packet for the next legal model-visible context hook
95
95
  - allow tool calls to proceed without secret/PII-based hook blocking
96
- - update `llm-wiki/outputs/questions/YYYY-MM-DD-live-qa.md` only for meaningful work turns
96
+ - update chunked `llm-wiki/outputs/questions/YYYY-MM-DD/live-qa-001.md` style archives only for meaningful work or structured decision turns
97
97
  - avoid automatic `wiki/queries/` and `wiki/decisions/` promotion in the default answer-first mode
98
98
  - queue durable cleanup candidates only for explicit documentation requests that were not reflected in durable wiki files, or when stale turn state is recovered
99
99
  - recover stale per-turn state into that queue on the next session start or prompt submit when the previous stop hook did not complete
@@ -112,6 +112,7 @@ Most users should not need these during daily Claude Code/Codex work. They exist
112
112
  - Diagnostics: `llm-wiki doctor`, `llm-wiki status`, `llm-wiki version`
113
113
  - Manual: `llm-wiki manual`
114
114
  - Agent maintenance helpers: `llm-wiki context`, `llm-wiki lint`, `llm-wiki consolidate`, `llm-wiki maintenance`
115
+ - Live Q&A archive helper: `llm-wiki archive-questions --workspace <project> [--date YYYY-MM-DD] [--dry-run]`
115
116
  - Cleanup: `llm-wiki uninstall`
116
117
 
117
118
  `llm-wiki manual` prints the full package manual from `docs/manual.md`. Keep that document current when adding public commands, options, hook behavior, directory conventions, security policy, or update flows.
@@ -134,6 +135,8 @@ Installed npm runtimes also perform a cached update notice check from hooks whil
134
135
 
135
136
  `llm-wiki maintenance` prints the pending queue and review due status from `llm-wiki/outputs/maintenance/queue.md`. Hooks create only selective candidates; the active agent should merge reusable items into existing durable wiki pages and mark queue items `done` or `skipped` without delaying unrelated user answers. Periodic maintenance is a soft agent-side reminder, not a user command loop.
136
137
 
138
+ `llm-wiki archive-questions` splits older legacy `llm-wiki/outputs/questions/YYYY-MM-DD-live-qa.md` files into the chunked `llm-wiki/outputs/questions/YYYY-MM-DD/` layout. It preserves the original under `outputs/questions/archive/originals/` with a SHA-256 sidecar and replaces the legacy file with a short pointer stub. Use `--dry-run` first when reviewing a large archive.
139
+
137
140
  `llm-wiki projects --workspace /apps` lists project roots that already have `llm-wiki-kit` state or an older `llm-wiki/wiki/index.md`, and shows the update commands to run. `llm-wiki update --workspace /apps` updates the global runtime once, then reapplies managed templates across every known or discovered project root under `/apps`.
138
141
 
139
142
  For `llm-wiki-kit` code releases, source tests are not enough. After code changes, publish the package, install the newly published version, and verify the installed CLI and hooks with `version`, `status`, `doctor`, `update`, `lint`, and hook smoke checks before calling the release complete.
package/docs/concepts.md CHANGED
@@ -18,11 +18,12 @@ The important behavior is a loop:
18
18
  2. `memory.md`, `index.md`, and relevant wiki context are injected automatically with an answer-first instruction.
19
19
  3. The user works normally; no extra command loop is required.
20
20
  4. Hooks gather redacted prompt/tool/result summaries.
21
- 5. At stop/session end, hooks append redacted live Q&A only for meaningful work turns.
22
- 6. Durable wiki promotion is selective: explicit record/document requests should be handled by the active agent in existing wiki pages; the hook queues review only when such a request was not reflected in durable files.
23
- 7. At the next start/prompt after an abrupt shutdown, hooks can recover stale turn state into `outputs/maintenance/queue.md`.
24
- 8. When reusable knowledge appears, the active Claude Code/Codex agent folds approved facts into existing durable wiki pages instead of leaving everything as one-off Q&A.
25
- 9. Future sessions start from the improved wiki instead of relying on long chat history.
21
+ 5. At stop/session end, hooks append redacted chunked live Q&A only for turns with work evidence or structured decision/debugging conclusions.
22
+ 6. Simple answers, status checks, and keyword-only responses stay out of live Q&A and durable wiki by default.
23
+ 7. Durable wiki promotion is selective: explicit record/document requests should be handled by the active agent in existing wiki pages; the hook queues review only when such a request was not reflected in durable files.
24
+ 8. At the next start/prompt after an abrupt shutdown, hooks can recover stale turn state into `outputs/maintenance/queue.md`.
25
+ 9. When reusable knowledge appears, the active Claude Code/Codex agent folds approved facts into existing durable wiki pages instead of leaving everything as one-off Q&A.
26
+ 10. Future sessions start from the improved wiki instead of relying on long chat history.
26
27
 
27
28
  The kit is a template/runtime repository. It must not centralize project wiki contents.
28
29
 
@@ -44,7 +44,7 @@ The hook records redacted turn summaries but does not deny tool calls only becau
44
44
 
45
45
  At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, performs a cached npm update notice check for npm installs, then injects functional compact context. The context still uses `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, relevant wiki/search state, operating rules, maintenance signals, passive runtime update status, and managed-template cleanup notes; the hook formats those signals so they are usable if shown in the Claude Code UI. At `UserPromptSubmit`, it recovers stale turn state, searches wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, performs the same cached update notice check, and injects the smallest useful functional compact context set. Update notice cache is scoped by npm command, and maintenance reminders are shown only when the prompt is wiki/maintenance related or matches a queue topic.
46
46
 
47
- `PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` classifies the current turn before compaction: simple turns record only a context note, meaningful work writes a live Q&A checkpoint, and durable candidates write both a checkpoint and a maintenance queue item. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event. `PostCompact` stores the redacted compact summary as a context note and prepares any pending recovery packet without returning model-visible context directly. In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` and `SessionEnd` append live Q&A only for meaningful work turns and do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked to record or document durable knowledge and no durable wiki update is detected, `Stop`/`SessionEnd` queue a pending maintenance item for agent review. `Stop` and `SessionEnd` then clear the per-session turn buffer; `SubagentStop` does not.
47
+ `PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` classifies the current turn before compaction: simple turns record only a context note, work-evidence or structured-decision turns write a chunked live Q&A checkpoint, and explicit durable candidates write a maintenance queue item only when no durable wiki update is detected. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event. `PostCompact` stores the redacted compact summary as a context note and prepares any pending recovery packet without returning model-visible context directly. In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` and `SessionEnd` append chunked live Q&A only for work-evidence or structured-decision turns and do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked to record or document durable knowledge and no durable wiki update is detected, `Stop`/`SessionEnd` queue a pending maintenance item for agent review. `Stop` and `SessionEnd` then clear the per-session turn buffer; `SubagentStop` does not.
48
48
 
49
49
  Set `LLM_WIKI_KIT_AUTO_PROJECT_UPDATE=0` only while diagnosing automatic managed-template refresh behavior.
50
50
  Set `LLM_WIKI_KIT_UPDATE_NOTICE=0` only while suppressing the cached passive runtime update status.
@@ -33,9 +33,9 @@ Expected behavior:
33
33
  - `UserPromptSubmit` recovers stale turn state, searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, performs the same cached update notice check, and injects the smallest useful functional compact context set. Update notice cache is scoped by npm command, and maintenance reminders are shown only when the prompt is wiki/maintenance related or matches a queue topic.
34
34
  - `PreToolUse` records redacted tool summaries without blocking tool calls.
35
35
  - `PostToolUse` records redacted tool summaries in a turn buffer.
36
- - `PreCompact` classifies the current turn before compaction. Simple turns record only a context note; meaningful work writes a live Q&A checkpoint; durable candidates write both a checkpoint and a maintenance queue item. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event.
36
+ - `PreCompact` classifies the current turn before compaction. Simple turns record only a context note; work-evidence or structured-decision turns write a chunked live Q&A checkpoint; explicit durable candidates write a maintenance queue item only when no durable wiki update is detected. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event.
37
37
  - `PostCompact` stores the redacted compact summary as a context note and prepares any pending compact recovery packet. It does not return `hookSpecificOutput.additionalContext`, because Codex `PostCompact` only supports common output fields.
38
- - In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` appends live Q&A only for meaningful work turns and does not auto-create `wiki/queries/` or `wiki/decisions/`.
38
+ - In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` appends chunked live Q&A only for work-evidence or structured-decision turns and does not auto-create `wiki/queries/` or `wiki/decisions/`.
39
39
  - If the user explicitly asked to record or document durable knowledge and no durable wiki update is detected, `Stop` queues a pending maintenance item for agent review.
40
40
  - `Stop` clears the per-session turn buffer after recording. `SubagentStop` leaves the parent turn buffer available for the final stop event.
41
41
 
package/docs/manual.md CHANGED
@@ -63,9 +63,9 @@ llm-wiki/
63
63
  설치된 hook은 다음 일을 자동으로 수행한다.
64
64
 
65
65
  - session start, instructions loaded, prompt submit 시점에 functional compact context를 주입한다. `wiki/memory.md`, `wiki/index.md`, 관련 wiki 검색 결과, maintenance signal, update status, compact recovery packet은 계속 사용하되 사용자 화면에 보일 수 있는 hook context는 필요한 정보 중심으로 정제한다.
66
- - pre compact 시점에는 현재 turn을 분류하고, simple turn은 context note만 남기며, meaningful/durable turn redacted live Q&A checkpoint 필요한 maintenance queue 후보를 남긴다. 저장 실패 시에도 compact는 진행시키고, 중요한 내용만 recovery packet으로 준비한다.
66
+ - pre compact 시점에는 현재 turn을 분류하고, simple turn은 context note만 남기며, 실제 작업 evidence가 있거나 구조화된 decision 결과가 있는 turn redacted live Q&A checkpoint 남긴다. 명시적 durable 기록 요청이 durable wiki에 반영되지 않았을 때만 maintenance queue 후보를 남긴다. 저장 실패 시에도 compact는 진행시키고, 중요한 내용만 recovery packet으로 준비한다.
67
67
  - prompt/tool/result summary를 redaction한 뒤 turn buffer에 기록한다.
68
- - 의미 있는 작업 turn만 `outputs/questions/YYYY-MM-DD-live-qa.md`에 live Q&A 남긴다.
68
+ - 의미 있는 작업/결정 turn만 `outputs/questions/YYYY-MM-DD/live-qa-001.md` 같은 chunked live Q&A archive에 남긴다.
69
69
  - 기본 answer-first mode에서는 `wiki/queries/`와 `wiki/decisions/`를 매 turn 자동 생성하지 않는다.
70
70
  - 사용자가 명시적으로 문서화/기록을 요청했는데 durable wiki 반영이 없으면 `outputs/maintenance/queue.md`에 정리 후보를 남긴다.
71
71
  - 이전 session이 깨끗하게 종료되지 않았을 때 stale turn state를 maintenance queue로 복구할 수 있다.
@@ -86,9 +86,9 @@ LLM_WIKI_KIT_CAPTURE_MODE=answer-first
86
86
 
87
87
  `answer-first` mode는 현재 답변을 우선한다.
88
88
 
89
- - 단순 Q&A 상태 확인은 durable wiki로 승격하지 않는다.
90
- - 의미 있는 작업 turn은 live Q&A archive에 남길 수 있다.
91
- - explicit durable request가 빠졌을 때만 selective maintenance queue를 만든다.
89
+ - 단순 Q&A, 상태 확인, 키워드만 포함된 답변은 durable wiki나 live Q&A archive로 승격하지 않는다.
90
+ - tool evidence, changed-file evidence, verification, 구조화된 `Decision:`/`Root cause:` 같은 결론이 있는 turn은 live Q&A archive에 남길 수 있다.
91
+ - explicit durable request가 durable wiki에 반영되지 않았을 때만 selective maintenance queue를 만든다.
92
92
  - reusable fact는 active agent가 기존 wiki 문서에 병합한다.
93
93
 
94
94
  과거 호환용 eager mode도 남아 있다.
@@ -309,6 +309,7 @@ llm-wiki lint --workspace /path/to/project
309
309
  - stale/archived page의 supersession/link discoverability
310
310
  - outdated managed rules/templates
311
311
  - stale or oversized maintenance queue
312
+ - oversized legacy live Q&A files or oversized live Q&A chunks
312
313
 
313
314
  broken links, invalid source IDs, secret-like content는 error이며 exit code 1을 반환한다. metadata/discoverability gap은 warning이다.
314
315
 
@@ -341,6 +342,18 @@ llm-wiki maintenance --workspace /path/to/project --json
341
342
 
342
343
  JSON 출력에는 `reviewDue`, `reviewReasons`, `pendingCount`, `stalePendingCount`, `health`, `recommendedCommands`가 포함된다. Review due 조건은 마지막 review 후 14일 경과, pending 5개 이상, stale/result-missing pending item, lint warning/error, `memory.md` near-budget 이상, wiki page count가 search cap의 80% 이상인 경우다. Hook은 due 상태를 `SessionStart`/`InstructionsLoaded`에서만 짧게 보여주고, `UserPromptSubmit`에서는 사용자가 wiki/maintenance/정리 관련 질문을 했을 때만 보여준다. 현재 답변을 중단하거나 정리를 강제하지 않는다.
343
344
 
345
+ ### `llm-wiki archive-questions`
346
+
347
+ legacy daily live Q&A 파일을 chunked archive layout으로 분할한다.
348
+
349
+ ```bash
350
+ llm-wiki archive-questions --workspace /path/to/project --dry-run
351
+ llm-wiki archive-questions --workspace /path/to/project --date 2026-06-07
352
+ llm-wiki archive-questions --workspace /path/to/project --date 2026-06-07 --json
353
+ ```
354
+
355
+ 대상은 `outputs/questions/YYYY-MM-DD-live-qa.md` 형식의 기존 파일이다. 실행하면 원본은 `outputs/questions/archive/originals/` 아래에 SHA-256 sidecar와 함께 보존하고, 새 기록은 `outputs/questions/YYYY-MM-DD/index.md`와 `live-qa-001.md` style chunk들로 나눈다. 기존 top-level 파일은 새 위치를 가리키는 짧은 stub로 대체한다. `--dry-run`은 어떤 파일도 쓰지 않고 split 계획만 출력한다.
356
+
344
357
  Agent checklist:
345
358
 
346
359
  ```bash
@@ -104,6 +104,7 @@ llm-wiki context "search phrase" --workspace /path/to/project
104
104
  llm-wiki lint --workspace /path/to/project
105
105
  llm-wiki consolidate --workspace /path/to/project
106
106
  llm-wiki maintenance --workspace /path/to/project
107
+ llm-wiki archive-questions --workspace /path/to/project --dry-run
107
108
  ```
108
109
 
109
110
  `status` is offline and answers whether the local installation is internally consistent:
@@ -143,7 +144,9 @@ After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working
143
144
 
144
145
  Daily use should be Claude Code/Codex first. The user should not need to run a chain of `llm-wiki` commands while working. Hooks inject context automatically, but the current user answer takes priority over wiki cleanup. The active agent updates durable wiki pages when reusable project knowledge appears and the turn's importance or user consent justifies persistence. Hook context policy is function-first: memory, search, maintenance, and update signals remain available, while user-visible context is formatted as functional compact context instead of a raw dump.
145
146
 
146
- In the default `LLM_WIKI_KIT_CAPTURE_MODE=answer-first` mode, `Stop` and `SessionEnd` append live Q&A only for meaningful work turns. They do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked for recording/documentation and no durable wiki update is detected, a pending cleanup candidate is written to `llm-wiki/outputs/maintenance/queue.md`. `PreCompact` performs the same answer-first classification before context compaction: simple turns get only a context note, archive-worthy turns get a live Q&A checkpoint, and durable candidates get a checkpoint plus queue item. If checkpoint storage fails, compaction still proceeds and the hook prepares an important-only compact recovery packet for the next legal context-injection event. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete. `SessionStart` injects a one-item queue summary; `UserPromptSubmit` injects a soft reminder only when the prompt is wiki/maintenance related or matches a queue topic. This is a recovery and reminder layer, not a full transcript capture path.
147
+ In the default `LLM_WIKI_KIT_CAPTURE_MODE=answer-first` mode, `Stop` and `SessionEnd` append live Q&A only for meaningful work evidence or structured decision turns. Simple answers, status checks, and keyword-only responses are not archived. Live Q&A uses chunked files under `llm-wiki/outputs/questions/YYYY-MM-DD/` and rolls over by line/byte budget. Hooks do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked for recording/documentation and no durable wiki update is detected, a pending cleanup candidate is written to `llm-wiki/outputs/maintenance/queue.md`. `PreCompact` performs the same answer-first classification before context compaction: simple turns get only a context note, archive-worthy turns get a live Q&A checkpoint, and explicit durable candidates get a checkpoint plus queue item only when needed. If checkpoint storage fails, compaction still proceeds and the hook prepares an important-only compact recovery packet for the next legal context-injection event. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete. `SessionStart` injects a one-item queue summary; `UserPromptSubmit` injects a soft reminder only when the prompt is wiki/maintenance related or matches a queue topic. This is a recovery and reminder layer, not a full transcript capture path.
148
+
149
+ Use `llm-wiki archive-questions --workspace <project> --dry-run` to review splitting legacy `outputs/questions/YYYY-MM-DD-live-qa.md` files into the chunked layout. Running it without `--dry-run` preserves the original under `outputs/questions/archive/originals/` with a checksum sidecar and replaces the legacy file with a pointer stub.
147
150
 
148
151
  Pre-compact preservation defaults to `LLM_WIKI_KIT_PRECOMPACT_ENFORCEMENT=limited`, but compaction is never blocked by llm-wiki-kit. `limited` and `soft` emit non-blocking failure warnings, and `off` suppresses failure output. `LLM_WIKI_KIT_PRECOMPACT_TRANSCRIPT_TAIL_BYTES` controls the small bounded transcript tail used for checkpoint context. Authentication values and the raw transcript path are redacted before storage.
149
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-wiki-kit",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "description": "Hook-first living LLM Wiki runtime for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "files": [
@@ -2,7 +2,9 @@ const NOT_CAPTURED = '(not captured)';
2
2
 
3
3
  const EXPLICIT_DURABLE_RE = /(?:기록해|기록해줘|문서화|문서화해|wiki에\s*남겨|위키에\s*남겨|wiki[^.\n]{0,40}(?:남겨|저장|기록)|위키[^.\n]{0,40}(?:남겨|저장|기록)|remember\s+this|save\s+this|document\s+this|record\s+this|persist\s+this)/i;
4
4
 
5
- const DURABLE_KEYWORD_RE = /(?:debug|bug|fix|failure|failed|error|root cause|architecture|runtime|hook|install|update|deploy|release|publish|migration|security|policy|procedure|decision|decided|verification|test|lint|build|doctor|디버깅|버그|오류|에러|실패|수정|원인|아키텍처|구조|런타임|훅|설치|업데이트|배포|릴리스|마이그레이션|보안|정책|절차|결정|선택|채택|확정|검증|테스트|문서|위키|wiki)/i;
5
+ const DURABLE_KEYWORD_RE = /(?:root cause|architecture|policy|procedure|decision|decided|migration|security|디버깅|원인|아키텍처|구조|정책|절차|결정|선택|채택|확정)/i;
6
+
7
+ const DURABLE_CONCLUSION_RE = /(?:^|\n)\s*(?:Decision|Decided|Policy|Procedure|Root cause|Resolution|Fix|Verification|결정|정책|절차|원인|해결|수정|검증)\s*[::-]/i;
6
8
 
7
9
  const MAINTENANCE_QUERY_RE = /(?:llm-wiki|wiki|위키|maintenance|maintain|문서|문서화|기록|정리|consolidate|lint|queue|AGENTS\.md)/i;
8
10
 
@@ -44,6 +46,11 @@ export function hasDurableKeyword(value) {
44
46
  return DURABLE_KEYWORD_RE.test(text);
45
47
  }
46
48
 
49
+ export function hasDurableConclusion(value) {
50
+ const text = typeof value === 'string' ? value : entrySearchText(value);
51
+ return DURABLE_CONCLUSION_RE.test(text);
52
+ }
53
+
47
54
  export function isMaintenanceRelatedQuery(query, pendingItems = []) {
48
55
  const text = capturedText(query);
49
56
  if (!text) return false;
@@ -107,37 +114,30 @@ export function classifyTurn(entry, eventName = '') {
107
114
  const hasWork = Boolean(work);
108
115
  const hasFiles = Boolean(changedFiles);
109
116
  const hasVerification = Boolean(verification);
110
- const longWork = work.length > 800;
111
- const durableKeyword = hasDurableKeyword(text);
112
- const simple =
113
- !hasWork &&
114
- !hasFiles &&
115
- !hasVerification &&
116
- question.length <= 120 &&
117
- result.length <= 800 &&
118
- !durableKeyword;
119
-
120
- if (simple) {
117
+ const hasWorkEvidence = hasWork || hasFiles || hasVerification;
118
+ const durableConclusion = hasDurableConclusion(text);
119
+
120
+ if (hasWorkEvidence) {
121
121
  return {
122
- kind: 'simple',
123
- archive: false,
122
+ kind: hasDetectedDurableWikiChange(entry) ? 'durable-updated' : 'work',
123
+ archive: true,
124
124
  suggestDurable: false,
125
125
  queueIfMissingDurable: false,
126
126
  };
127
127
  }
128
128
 
129
- if (durableKeyword) {
129
+ if (durableConclusion) {
130
130
  return {
131
- kind: 'suggest-durable',
131
+ kind: 'decision',
132
132
  archive: true,
133
- suggestDurable: true,
133
+ suggestDurable: false,
134
134
  queueIfMissingDurable: false,
135
135
  };
136
136
  }
137
137
 
138
138
  return {
139
- kind: hasWork || hasFiles || hasVerification || longWork || result.length > 800 ? 'archive-only' : 'simple',
140
- archive: hasWork || hasFiles || hasVerification || longWork || result.length > 800,
139
+ kind: 'simple',
140
+ archive: false,
141
141
  suggestDurable: false,
142
142
  queueIfMissingDurable: false,
143
143
  };
package/src/cli.js CHANGED
@@ -12,6 +12,7 @@ import { migrate } from './migrate.js';
12
12
  import { postUpdate, update } from './update.js';
13
13
  import { buildContextPack, formatContextPack } from './wiki-search.js';
14
14
  import { formatLintResult, runLint } from './wiki-lint.js';
15
+ import { archiveQuestions, formatArchiveQuestionsResult } from './live-qa.js';
15
16
 
16
17
  function parseOptions(args) {
17
18
  const options = {};
@@ -32,6 +33,13 @@ function parseOptions(args) {
32
33
  } else if (arg === '--to') {
33
34
  options.to = optionValue(arg, i);
34
35
  i += 1;
36
+ } else if (arg === '--date') {
37
+ const value = optionValue(arg, i);
38
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
39
+ throw new Error('--date must use YYYY-MM-DD');
40
+ }
41
+ options.date = value;
42
+ i += 1;
35
43
  } else if (arg === '--timeout-ms') {
36
44
  const value = optionValue(arg, i);
37
45
  const timeout = Number(value);
@@ -122,6 +130,7 @@ Usage:
122
130
  llm-wiki lint --workspace <project>
123
131
  llm-wiki consolidate --workspace <project> [--dry-run]
124
132
  llm-wiki maintenance --workspace <project> [--json]
133
+ llm-wiki archive-questions --workspace <project> [--date YYYY-MM-DD] [--dry-run] [--json]
125
134
  `);
126
135
  return;
127
136
  }
@@ -240,6 +249,12 @@ Usage:
240
249
  return;
241
250
  }
242
251
 
252
+ if (command === 'archive-questions') {
253
+ const projectRoot = resolve(options.workspace || process.cwd());
254
+ printJsonOrText(await archiveQuestions(projectRoot, options), options, formatArchiveQuestionsResult);
255
+ return;
256
+ }
257
+
243
258
  if (command === 'hook') {
244
259
  const provider = rest[0] || 'codex';
245
260
  const eventName = rest[1];
package/src/live-qa.js ADDED
@@ -0,0 +1,321 @@
1
+ import { copyFile, readdir } from 'fs/promises';
2
+ import { basename, dirname, join, relative } from 'path';
3
+ import {
4
+ appendText,
5
+ ensureDir,
6
+ exists,
7
+ readText,
8
+ sha256,
9
+ timeKst,
10
+ todayKst,
11
+ writeText,
12
+ } from './fs-utils.js';
13
+ import { redactText } from './redaction.js';
14
+
15
+ export const DEFAULT_LIVE_QA_MAX_LINES = 500;
16
+ export const DEFAULT_LIVE_QA_MAX_BYTES = 80 * 1024;
17
+
18
+ const ARCHIVE_STUB_MARKER = '<!-- llm-wiki-kit:archived-live-qa -->';
19
+
20
+ function positiveInteger(value, fallback) {
21
+ const parsed = Number(value);
22
+ if (!Number.isInteger(parsed) || parsed < 1) return fallback;
23
+ return parsed;
24
+ }
25
+
26
+ export function liveQaMaxLines(env = process.env) {
27
+ return positiveInteger(env.LLM_WIKI_KIT_LIVE_QA_MAX_LINES, DEFAULT_LIVE_QA_MAX_LINES);
28
+ }
29
+
30
+ export function liveQaMaxBytes(env = process.env) {
31
+ return positiveInteger(env.LLM_WIKI_KIT_LIVE_QA_MAX_BYTES, DEFAULT_LIVE_QA_MAX_BYTES);
32
+ }
33
+
34
+ export function liveQaDayDir(projectRoot, day = todayKst()) {
35
+ return join(projectRoot, 'llm-wiki', 'outputs', 'questions', day);
36
+ }
37
+
38
+ function liveQaLegacyPath(projectRoot, day) {
39
+ return join(projectRoot, 'llm-wiki', 'outputs', 'questions', `${day}-live-qa.md`);
40
+ }
41
+
42
+ function chunkName(number) {
43
+ return `live-qa-${String(number).padStart(3, '0')}.md`;
44
+ }
45
+
46
+ function chunkNumber(file) {
47
+ const match = file.match(/^live-qa-(\d+)\.md$/);
48
+ return match ? Number(match[1]) : null;
49
+ }
50
+
51
+ function countLines(text) {
52
+ if (!text) return 0;
53
+ return String(text).split(/\r?\n/).length;
54
+ }
55
+
56
+ function countBlocks(text) {
57
+ return (String(text || '').match(/^## \d{2}:\d{2} KST - /gm) || []).length;
58
+ }
59
+
60
+ async function listChunkFiles(dayDir) {
61
+ let entries = [];
62
+ try {
63
+ entries = await readdir(dayDir, { withFileTypes: true });
64
+ } catch {
65
+ return [];
66
+ }
67
+ return entries
68
+ .filter((entry) => entry.isFile() && chunkNumber(entry.name) !== null)
69
+ .map((entry) => entry.name)
70
+ .sort((a, b) => chunkNumber(a) - chunkNumber(b));
71
+ }
72
+
73
+ function exceedsLimit(existing, addition, options = {}) {
74
+ if (!existing) return false;
75
+ const maxLines = options.maxLines ?? liveQaMaxLines();
76
+ const maxBytes = options.maxBytes ?? liveQaMaxBytes();
77
+ return (
78
+ countLines(existing) + countLines(addition) > maxLines ||
79
+ Buffer.byteLength(existing, 'utf8') + Buffer.byteLength(addition, 'utf8') > maxBytes
80
+ );
81
+ }
82
+
83
+ async function targetChunkPath(projectRoot, day, block, options = {}) {
84
+ const dayDir = liveQaDayDir(projectRoot, day);
85
+ await ensureDir(dayDir);
86
+ const files = await listChunkFiles(dayDir);
87
+ let number = files.length > 0 ? chunkNumber(files.at(-1)) : 1;
88
+ let path = join(dayDir, chunkName(number));
89
+ const existing = await readText(path, '');
90
+ if (exceedsLimit(existing, block, options)) {
91
+ number += 1;
92
+ path = join(dayDir, chunkName(number));
93
+ }
94
+ return path;
95
+ }
96
+
97
+ export function formatLiveQaBlock(entry) {
98
+ return [
99
+ `\n## ${timeKst()} KST - ${entry.topic || 'session turn'}`,
100
+ '',
101
+ '### Question',
102
+ entry.question || '(not captured)',
103
+ '',
104
+ '### Work',
105
+ entry.work || '(not captured)',
106
+ '',
107
+ '### Result',
108
+ entry.result || '(not captured)',
109
+ '',
110
+ '### Changed files',
111
+ entry.changedFiles || '(not captured)',
112
+ '',
113
+ '### Verification',
114
+ entry.verification || '(not captured)',
115
+ '',
116
+ '### Follow-up',
117
+ entry.followUp || '작업/결정 중심으로 저장된 live Q&A 기록이다. 재사용 가능한 사실은 사용자가 원하거나 명확히 중요할 때만 기존 durable wiki 문서에 합친다.',
118
+ '',
119
+ ].join('\n');
120
+ }
121
+
122
+ export async function refreshLiveQaIndex(projectRoot, day = todayKst()) {
123
+ const dayDir = liveQaDayDir(projectRoot, day);
124
+ const files = await listChunkFiles(dayDir);
125
+ const rows = [];
126
+ let totalBlocks = 0;
127
+ for (const file of files) {
128
+ const rel = file;
129
+ const text = await readText(join(dayDir, file), '');
130
+ const blocks = countBlocks(text);
131
+ totalBlocks += blocks;
132
+ rows.push(`- [${rel}](${rel}) - ${blocks} turn(s), ${countLines(text)} line(s)`);
133
+ }
134
+ const content = [
135
+ `# Live Q&A ${day}`,
136
+ '',
137
+ 'Chunked live Q&A archive for meaningful work and decision turns.',
138
+ '',
139
+ `- chunks: ${files.length}`,
140
+ `- turns: ${totalBlocks}`,
141
+ `- updated_at: ${new Date().toISOString()}`,
142
+ '',
143
+ '## Chunks',
144
+ '',
145
+ rows.length > 0 ? rows.join('\n') : '(none)',
146
+ '',
147
+ ].join('\n');
148
+ await writeText(join(dayDir, 'index.md'), content);
149
+ }
150
+
151
+ export async function appendLiveQa(projectRoot, entry, options = {}) {
152
+ const day = todayKst();
153
+ const block = redactText(formatLiveQaBlock(entry), 12000);
154
+ const path = await targetChunkPath(projectRoot, day, block, options);
155
+ await appendText(path, block);
156
+ await refreshLiveQaIndex(projectRoot, day);
157
+ return path;
158
+ }
159
+
160
+ function splitLiveQaBlocks(text) {
161
+ const blocks = [];
162
+ let current = [];
163
+ for (const line of String(text || '').replace(/\r\n/g, '\n').split('\n')) {
164
+ if (/^## \d{2}:\d{2} KST - /.test(line) && current.some((item) => item.trim())) {
165
+ blocks.push(`\n${current.join('\n').trim()}\n`);
166
+ current = [line];
167
+ } else {
168
+ current.push(line);
169
+ }
170
+ }
171
+ if (current.some((item) => item.trim())) {
172
+ blocks.push(`\n${current.join('\n').trim()}\n`);
173
+ }
174
+ return blocks;
175
+ }
176
+
177
+ function packBlocks(blocks, options = {}) {
178
+ const chunks = [];
179
+ let current = '';
180
+ for (const block of blocks) {
181
+ if (current && exceedsLimit(current, block, options)) {
182
+ chunks.push(current);
183
+ current = block;
184
+ } else {
185
+ current += block;
186
+ }
187
+ }
188
+ if (current) chunks.push(current);
189
+ return chunks;
190
+ }
191
+
192
+ async function writeGeneratedFile(path, content) {
193
+ const current = await readText(path, null);
194
+ if (current !== null && current !== content) {
195
+ throw new Error(`refusing to overwrite existing generated target with different content: ${path}`);
196
+ }
197
+ if (current === content) return false;
198
+ await writeText(path, content);
199
+ return true;
200
+ }
201
+
202
+ async function backupTarget(projectRoot, sourcePath, checksum) {
203
+ const base = join(projectRoot, 'llm-wiki', 'outputs', 'questions', 'archive', 'originals', basename(sourcePath));
204
+ if (!(await exists(base))) return base;
205
+ const currentChecksum = await readText(`${base}.sha256`, '');
206
+ if (currentChecksum.includes(checksum)) return base;
207
+ const parsed = basename(sourcePath, '.md');
208
+ return join(projectRoot, 'llm-wiki', 'outputs', 'questions', 'archive', 'originals', `${parsed}-${checksum.slice(0, 12)}.md`);
209
+ }
210
+
211
+ async function archiveOne(projectRoot, day, options = {}) {
212
+ const sourcePath = liveQaLegacyPath(projectRoot, day);
213
+ const sourceRel = relative(projectRoot, sourcePath).split('\\').join('/');
214
+ if (!(await exists(sourcePath))) {
215
+ return { date: day, status: 'missing', source: sourceRel, chunks: [] };
216
+ }
217
+
218
+ const text = await readText(sourcePath, '');
219
+ if (text.includes(ARCHIVE_STUB_MARKER)) {
220
+ return { date: day, status: 'already-archived', source: sourceRel, chunks: [] };
221
+ }
222
+
223
+ const blocks = splitLiveQaBlocks(text);
224
+ if (blocks.length === 0) {
225
+ return { date: day, status: 'empty', source: sourceRel, chunks: [] };
226
+ }
227
+
228
+ const checksum = sha256(text);
229
+ const backupPath = await backupTarget(projectRoot, sourcePath, checksum);
230
+ const backupRel = relative(projectRoot, backupPath).split('\\').join('/');
231
+ const chunks = packBlocks(blocks, options);
232
+ const dayDir = liveQaDayDir(projectRoot, day);
233
+ const existingChunks = await listChunkFiles(dayDir);
234
+ const firstChunkNumber = existingChunks.length > 0 ? chunkNumber(existingChunks.at(-1)) + 1 : 1;
235
+ const chunkTargets = chunks.map((content, index) => ({
236
+ path: join(dayDir, chunkName(firstChunkNumber + index)),
237
+ rel: relative(projectRoot, join(dayDir, chunkName(firstChunkNumber + index))).split('\\').join('/'),
238
+ content,
239
+ blocks: countBlocks(content),
240
+ lines: countLines(content),
241
+ }));
242
+
243
+ const result = {
244
+ date: day,
245
+ status: options.dryRun ? 'planned' : 'archived',
246
+ source: sourceRel,
247
+ original: backupRel,
248
+ checksum,
249
+ blocks: blocks.length,
250
+ chunks: chunkTargets.map(({ rel, blocks: blockCount, lines }) => ({ path: rel, blocks: blockCount, lines })),
251
+ };
252
+ if (options.dryRun) return result;
253
+
254
+ await ensureDir(dirname(backupPath));
255
+ if (!(await exists(backupPath))) await copyFile(sourcePath, backupPath);
256
+ await writeText(`${backupPath}.sha256`, `${checksum} ${basename(sourcePath)}\n`);
257
+ await ensureDir(dayDir);
258
+ for (const target of chunkTargets) {
259
+ await writeGeneratedFile(target.path, target.content);
260
+ }
261
+ await refreshLiveQaIndex(projectRoot, day);
262
+ const stub = [
263
+ '# Live Q&A Archived',
264
+ ARCHIVE_STUB_MARKER,
265
+ '',
266
+ 'This legacy daily live Q&A file was archived into chunked files.',
267
+ '',
268
+ `- original: ${backupRel}`,
269
+ `- sha256: ${checksum}`,
270
+ `- index: llm-wiki/outputs/questions/${day}/index.md`,
271
+ '',
272
+ ].join('\n');
273
+ await writeText(sourcePath, stub);
274
+ return result;
275
+ }
276
+
277
+ async function datesToArchive(projectRoot, options = {}) {
278
+ if (options.date) return [options.date];
279
+ const questionsDir = join(projectRoot, 'llm-wiki', 'outputs', 'questions');
280
+ let entries = [];
281
+ try {
282
+ entries = await readdir(questionsDir, { withFileTypes: true });
283
+ } catch {
284
+ return [];
285
+ }
286
+ return entries
287
+ .filter((entry) => entry.isFile())
288
+ .map((entry) => entry.name.match(/^(\d{4}-\d{2}-\d{2})-live-qa\.md$/)?.[1])
289
+ .filter(Boolean)
290
+ .sort();
291
+ }
292
+
293
+ export async function archiveQuestions(projectRoot, options = {}) {
294
+ const dates = await datesToArchive(projectRoot, options);
295
+ const files = [];
296
+ for (const day of dates) {
297
+ files.push(await archiveOne(projectRoot, day, options));
298
+ }
299
+ return {
300
+ workspace: projectRoot,
301
+ dryRun: Boolean(options.dryRun),
302
+ date: options.date || null,
303
+ files,
304
+ };
305
+ }
306
+
307
+ export function formatArchiveQuestionsResult(result) {
308
+ const lines = [
309
+ 'llm-wiki archive-questions',
310
+ `- workspace: ${result.workspace}`,
311
+ `- dry-run: ${result.dryRun ? 'yes' : 'no'}`,
312
+ `- files: ${result.files.length}`,
313
+ ];
314
+ for (const file of result.files) {
315
+ lines.push(`- ${file.date}: ${file.status}; blocks=${file.blocks || 0}; chunks=${file.chunks.length}`);
316
+ for (const chunk of file.chunks) {
317
+ lines.push(` - ${chunk.path} (${chunk.blocks} turn(s), ${chunk.lines} line(s))`);
318
+ }
319
+ }
320
+ return lines.join('\n');
321
+ }
package/src/project.js CHANGED
@@ -14,6 +14,7 @@ import { normalizeForStorage, redactText, summarizeForStorage } from './redactio
14
14
  import { gitignore, indexPage, llmWikiAgents, logPage, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
15
15
  import { formatProjectMaintenanceContext, inspectProjectState, recordManagedTemplates } from './project-state.js';
16
16
  import { buildContextPack, formatHookContextPack, searchWiki as searchWikiWithIndex } from './wiki-search.js';
17
+ import { appendLiveQa as appendChunkedLiveQa } from './live-qa.js';
17
18
 
18
19
  export async function bootstrapProject(projectRoot, options = {}) {
19
20
  if (process.env.LLM_WIKI_KIT_DISABLE_BOOTSTRAP === '1') return { created: false };
@@ -97,32 +98,7 @@ function hasCapturedQuestion(entry) {
97
98
  }
98
99
 
99
100
  export async function appendLiveQa(projectRoot, entry) {
100
- const day = todayKst();
101
- const path = join(projectRoot, 'llm-wiki', 'outputs', 'questions', `${day}-live-qa.md`);
102
- const block = [
103
- `\n## ${timeKst()} KST - ${entry.topic || 'session turn'}`,
104
- '',
105
- '### Question',
106
- entry.question || '(not captured)',
107
- '',
108
- '### Work',
109
- entry.work || '(not captured)',
110
- '',
111
- '### Result',
112
- entry.result || '(not captured)',
113
- '',
114
- '### Changed files',
115
- entry.changedFiles || '(not captured)',
116
- '',
117
- '### Verification',
118
- entry.verification || '(not captured)',
119
- '',
120
- '### Follow-up',
121
- entry.followUp || '다음 작업에서 이 turn의 reusable fact가 있으면 기존 wiki 문서에 합치고, 일회성 기록은 이 Q&A에만 보존한다.',
122
- '',
123
- ].join('\n');
124
- await appendText(path, redactText(block, 12000));
125
- return path;
101
+ return appendChunkedLiveQa(projectRoot, entry);
126
102
  }
127
103
 
128
104
  export async function writeQueryPage(projectRoot, entry) {
package/src/templates.js CHANGED
@@ -14,10 +14,10 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
14
14
  - \`llm-wiki/wiki/\`는 agent가 관리하는 지식층이다. 결정, 구조, 디버깅, 개념, 절차, 맥락을 여기에 정리한다.
15
15
  - \`llm-wiki/wiki/memory.md\`는 짧은 핵심 기억이다. 긴 설명 대신 현재 상태와 중요한 문서 링크만 유지한다.
16
16
  - hook이 주입한 context를 참고하되, 현재 사용자 답변을 먼저 처리한다. 수동 확인이나 정리에는 \`llm-wiki context\`, \`llm-wiki lint\`, \`llm-wiki consolidate\`를 agent 보조 도구로 사용한다.
17
- - hook은 redacted raw envelope와 필요한 live Q&A 안전하게 남긴다. \`wiki/queries\`/\`wiki/decisions\` 자동 승격은 기본값이 아니며, durable 지식은 중요도와 동의 흐름에 따라 agent가 기존 정식 wiki 문서에 합친다.
17
+ - hook은 redacted raw envelope와 작업/결정 중심 live Q&A 안전하게 남긴다. 단순 답변, 상태 확인, 키워드만 포함된 응답은 live Q&A나 durable wiki로 승격하지 않는다. \`wiki/queries\`/\`wiki/decisions\` 자동 승격은 기본값이 아니며, durable 지식은 중요도와 동의 흐름에 따라 agent가 기존 정식 wiki 문서에 합친다.
18
18
  - hook은 종료/시작 경계에서 정말 필요한 정리 후보를 \`llm-wiki/outputs/maintenance/queue.md\`에 남길 수 있다. 정기 maintenance는 agent-side soft reminder이며, pending 항목은 현재 응답을 지연시키지 않는 범위에서 agent가 병합하거나 done/skipped로 표시한다.
19
- - 새 문서를 만들기 전에 기존 wiki 문서를 먼저 찾아 갱신한다. 반복해서 쓸 사실은 \`outputs/questions/\`에만 두지 말고 적절한 wiki 문서에 합친다.
20
- - 일회성 작업 기록은 필요할 때 \`llm-wiki/outputs/questions/\`에 보존하고, 재사용 가능한 사실/지식은 승인된 경우 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 반영한다.
19
+ - 새 문서를 만들기 전에 기존 wiki 문서를 먼저 찾아 갱신한다. 반복해서 쓸 사실은 chunked \`outputs/questions/\`에만 두지 말고 적절한 wiki 문서에 합친다.
20
+ - 일회성 작업/결정 기록은 필요할 때 \`llm-wiki/outputs/questions/YYYY-MM-DD/live-qa-001.md\` style chunk에 보존하고, 재사용 가능한 사실/지식은 승인된 경우 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 반영한다.
21
21
  - 검증 명령, 근거 파일, 불확실한 점을 함께 남긴다. 추론은 추론이라고 표시하고, 모순은 지우지 말고 Open Questions 또는 Contradictions에 남긴다.
22
22
  - 인증값, token, password, private key, \`.env\` 원문은 저장하지 않는다. 필요한 경우 redacted summary만 남긴다.
23
23
 
@@ -47,7 +47,7 @@ Codex와 Claude Code를 평소처럼 사용하는 동안 living Markdown LLM Wik
47
47
  - 근거 없는 내용을 사실처럼 쓰지 않는다. 추론은 명시한다.
48
48
  - 중요한 주장에는 \`source_ids\`, 파일 경로, 검증 명령 중 하나 이상을 남긴다.
49
49
  - 오래 남길 내용이 생기면 새 문서부터 만들지 말고 기존 \`wiki/\` 문서를 먼저 찾아 갱신한다.
50
- - 단순 답변, 상태 확인, 일회성 대화는 과도하게 \`wiki/queries\`나 maintenance로 승격하지 않는다.
50
+ - 단순 답변, 상태 확인, 키워드만 포함된 응답, 일회성 대화는 live Q&A, \`wiki/queries\`, maintenance로 승격하지 않는다.
51
51
  - 반복해서 쓸 지식은 중요도와 동의 흐름에 따라 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
52
52
  - hook이 만든 \`outputs/maintenance/queue.md\` pending 항목은 현재 요청과 관련 있거나 review due 안내가 있을 때 확인하고, 기존 정식 wiki 문서에 병합한 뒤 done 또는 skipped로 표시한다.
53
53
  - 정기 maintenance는 자동 수정이 아니라 agent review다. \`SessionStart\`/\`InstructionsLoaded\`에서만 짧게 안내되고, \`UserPromptSubmit\`에서는 사용자가 wiki/maintenance/정리 관련 질문을 한 경우에만 안내된다.
@@ -108,7 +108,7 @@ Generated by llm-wiki-kit.
108
108
 
109
109
  - 넓은 질문은 memory와 이 index에서 시작한다.
110
110
  - 새 문서를 만들기 전에 관련 기존 문서 3-7개를 먼저 확인한다.
111
- - 오래 쓸 사실/지식은 기존 정식 문서에 합치고, 일회성 기록은 outputs/questions에 둔다.
111
+ - 오래 쓸 사실/지식은 기존 정식 문서에 합치고, 작업/결정 중심 일회성 기록은 chunked outputs/questions에 둔다.
112
112
  - 관련 페이지가 생기면 \`[[page-or-topic]]\` 링크를 추가한다.
113
113
 
114
114
  <!-- llm-wiki-kit:index-start -->
@@ -184,14 +184,14 @@ export function procedure(name) {
184
184
  4. 정확한 근거가 필요할 때만 raw source를 확인한다.
185
185
  5. 검증된 사실과 추론을 분리한다.
186
186
  6. 수동 확인이 필요할 때만 \`llm-wiki context "<query>"\`를 쓴다. 과거 episodic query/context까지 봐야 할 때는 \`--include-episodic\`을 붙이고, archived/superseded page까지 봐야 할 때만 \`--include-archived\`를 붙인다.
187
- 7. 일회성 답변은 durable wiki로 승격하지 않는다. 필요한 작업 turn만 \`outputs/questions/\`에 남긴다.
187
+ 7. 일회성 답변, 상태 확인, 키워드만 포함된 응답은 durable wiki나 live Q&A로 승격하지 않는다. tool evidence, changed-file evidence, verification, 구조화된 decision/debugging 결론이 있는 작업 turn만 \`outputs/questions/YYYY-MM-DD/live-qa-001.md\` style chunk에 남긴다.
188
188
  8. 반복해서 쓸 사실/지식은 중요도와 동의 흐름에 따라 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
189
189
  `,
190
190
  'lint.md': `# Lint Procedure
191
191
 
192
192
  \`llm-wiki lint --workspace <project>\`는 agent가 필요할 때 쓰는 wiki 건강 점검 도구다. 사용자가 매번 실행해야 하는 명령이 아니다.
193
193
 
194
- 점검 대상: stale page, orphan page, broken wiki/Markdown link, unsafe source id, secret-like content, missing source, duplicate concept/title, unsupported claim, unresolved contradiction, outdated managed rule, memory budget, page count growth, hidden episodic/context growth, stale/archived discoverability, maintenance review due.
194
+ 점검 대상: stale page, orphan page, broken wiki/Markdown link, unsafe source id, secret-like content, missing source, duplicate concept/title, unsupported claim, unresolved contradiction, outdated managed rule, memory budget, page count growth, hidden episodic/context growth, stale/archived discoverability, maintenance review due, oversized legacy live Q&A, oversized live Q&A chunk.
195
195
 
196
196
  정기 maintenance는 사용자가 매 turn 실행할 필요가 없는 agent-side task다. 필요할 때 agent가 다음 순서로 확인한다:
197
197
 
package/src/wiki-lint.js CHANGED
@@ -4,6 +4,7 @@ import { exists, readText } from './fs-utils.js';
4
4
  import { maintenanceSummary } from './maintenance.js';
5
5
  import { inspectProjectState } from './project-state.js';
6
6
  import { hasSecretLikeText } from './redaction.js';
7
+ import { liveQaMaxBytes, liveQaMaxLines } from './live-qa.js';
7
8
  import {
8
9
  buildAliasMap,
9
10
  buildWikiGraph,
@@ -41,11 +42,57 @@ const CORE_PAGES = new Set(['wiki/index.md', 'wiki/log.md', 'wiki/memory.md']);
41
42
  const MEMORY_NEAR_BUDGET_BYTES = 20 * 1024;
42
43
  const HIDDEN_PAGE_WARNING_THRESHOLD = 50;
43
44
  const SEARCH_CAP_WARNING_RATIO = 0.8;
45
+ const ARCHIVED_LIVE_QA_MARKER = '<!-- llm-wiki-kit:archived-live-qa -->';
44
46
 
45
47
  function issue(severity, code, path, message) {
46
48
  return { severity, code, path, message };
47
49
  }
48
50
 
51
+ function countLines(text) {
52
+ if (!text) return 0;
53
+ return String(text).split(/\r?\n/).length;
54
+ }
55
+
56
+ function liveQaOversized(text) {
57
+ return countLines(text) > liveQaMaxLines() || Buffer.byteLength(text || '', 'utf8') > liveQaMaxBytes();
58
+ }
59
+
60
+ async function liveQaGrowthIssues(projectRoot) {
61
+ const issues = [];
62
+ const root = join(projectRoot, 'llm-wiki', 'outputs', 'questions');
63
+ let entries = [];
64
+ try {
65
+ entries = await readdir(root, { withFileTypes: true });
66
+ } catch {
67
+ return issues;
68
+ }
69
+ for (const entry of entries) {
70
+ const full = join(root, entry.name);
71
+ if (entry.isFile() && /^\d{4}-\d{2}-\d{2}-live-qa\.md$/.test(entry.name)) {
72
+ const text = await readText(full, '');
73
+ if (!text.includes(ARCHIVED_LIVE_QA_MARKER) && liveQaOversized(text)) {
74
+ issues.push(issue('warning', 'live-qa-legacy-oversized', `outputs/questions/${entry.name}`, 'legacy daily live Q&A file is oversized; run archive-questions to split it into chunks'));
75
+ }
76
+ }
77
+ if (entry.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry.name)) {
78
+ let chunkEntries = [];
79
+ try {
80
+ chunkEntries = await readdir(full, { withFileTypes: true });
81
+ } catch {
82
+ chunkEntries = [];
83
+ }
84
+ for (const chunk of chunkEntries) {
85
+ if (!chunk.isFile() || !/^live-qa-\d+\.md$/.test(chunk.name)) continue;
86
+ const text = await readText(join(full, chunk.name), '');
87
+ if (liveQaOversized(text)) {
88
+ issues.push(issue('warning', 'live-qa-chunk-oversized', `outputs/questions/${entry.name}/${chunk.name}`, 'live Q&A chunk exceeds the configured rollover budget'));
89
+ }
90
+ }
91
+ }
92
+ }
93
+ return issues;
94
+ }
95
+
49
96
  function isDateLike(value) {
50
97
  return value === 'unknown' || /^\d{4}-\d{2}-\d{2}$/.test(String(value || ''));
51
98
  }
@@ -291,6 +338,8 @@ export async function runLint(projectRoot, options = {}) {
291
338
  }
292
339
  }
293
340
 
341
+ issues.push(...await liveQaGrowthIssues(projectRoot));
342
+
294
343
  return lintResult(projectRoot, pages, issues);
295
344
  }
296
345