llm-wiki-kit 0.1.12 → 0.1.14

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
@@ -58,10 +58,13 @@ llm-wiki/
58
58
  │ ├── context/
59
59
  │ └── queries/
60
60
  ├── outputs/
61
+ │ ├── questions/
62
+ │ ├── reports/
63
+ │ └── maintenance/
61
64
  └── procedures/
62
65
  ```
63
66
 
64
- `raw/` is the immutable or redacted evidence layer. `wiki/` is the LLM-maintained knowledge layer. `wiki/memory.md` is the short hot index injected into hook context before deeper search results. `outputs/` stores live Q&A and requested reports. `.kit-state.json` records which runtime version last applied managed templates to the project. Text written by the kit is normalized to NFC so Korean filenames and content stay composed.
67
+ `raw/` is the immutable or redacted evidence layer. `wiki/` is the LLM-maintained knowledge layer. `wiki/memory.md` is the short hot index injected into hook context before deeper search results. `outputs/` stores live Q&A, requested reports, and pending wiki maintenance candidates. `.kit-state.json` records which runtime version last applied managed templates to the project. Text written by the kit is normalized to NFC so Korean filenames and content stay composed.
65
68
 
66
69
  ## Normal Use
67
70
 
@@ -76,6 +79,8 @@ The installed hooks:
76
79
  - update `llm-wiki/outputs/questions/YYYY-MM-DD-live-qa.md`
77
80
  - capture reusable-answer candidates into `llm-wiki/wiki/queries/`
78
81
  - promote decision-like turns into `llm-wiki/wiki/decisions/`
82
+ - queue durable cleanup candidates in `llm-wiki/outputs/maintenance/queue.md` at stop/session-end time
83
+ - recover stale per-turn state into that queue on the next session start or prompt submit when the previous stop hook did not complete
79
84
  - nudge the active LLM to fold reusable facts into existing wiki pages instead of leaving everything as one-off Q&A
80
85
  - automatically refresh managed rules/templates for older projects when the current runtime starts a session
81
86
 
@@ -87,10 +92,10 @@ Most users should not need these during daily Claude Code/Codex work. They exist
87
92
 
88
93
  - Install/update: `llm-wiki install`, `llm-wiki update`, `llm-wiki post-update`, `llm-wiki projects`
89
94
  - Diagnostics: `llm-wiki doctor`, `llm-wiki status`, `llm-wiki version`
90
- - Agent maintenance helpers: `llm-wiki context`, `llm-wiki lint`, `llm-wiki consolidate`
95
+ - Agent maintenance helpers: `llm-wiki context`, `llm-wiki lint`, `llm-wiki consolidate`, `llm-wiki maintenance`
91
96
  - Cleanup: `llm-wiki uninstall`
92
97
 
93
- `llm-wiki status` is an offline consistency check. It reports the installed runtime version, hook targets, whether the `llm-wiki` command on `PATH` resolves to the current runtime, whether the current workspace has the current managed templates applied, how many rules are auto-updateable, and how many managed-looking rules need agent cleanup.
98
+ `llm-wiki status` is an offline consistency check. It reports the installed runtime version, hook targets, whether the `llm-wiki` command on `PATH` resolves to the current runtime, whether the current workspace has the current managed templates applied, how many rules are auto-updateable, how many managed-looking rules need agent cleanup, and how many wiki maintenance items are pending.
94
99
 
95
100
  `llm-wiki update --check [--to <version-or-tag>]` is the online update check. It compares the installed package version with the npm registry target without changing files, and reports an available update only when the target version is newer than the installed version.
96
101
 
@@ -104,6 +109,8 @@ Most users should not need these during daily Claude Code/Codex work. They exist
104
109
 
105
110
  `llm-wiki consolidate` refreshes only generated marker blocks in `wiki/memory.md` and `wiki/index.md`. It is an agent maintenance helper, not a command users should run after every turn.
106
111
 
112
+ `llm-wiki maintenance` prints the pending queue from `llm-wiki/outputs/maintenance/queue.md`. Hooks create candidates; the active agent should merge reusable items into existing durable wiki pages and mark queue items `done` or `skipped`.
113
+
107
114
  `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`.
108
115
 
109
116
  After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working when they already point at the global npm package path. The next `SessionStart`/`InstructionsLoaded` hook automatically reapplies safe managed template updates for the active project root. Clearly generated older `llm-wiki/AGENTS.md` and procedure files are refreshed even when old state is missing; user-edited files are not overwritten and are surfaced to the active agent as cleanup work. If hooks point at a source checkout or stale shim, run `llm-wiki post-update --workspace <project>` or `llm-wiki install --workspace <project>` once to reconnect them.
package/docs/concepts.md CHANGED
@@ -19,8 +19,9 @@ The important behavior is a loop:
19
19
  3. The user works normally; no extra command loop is required.
20
20
  4. Hooks gather redacted prompt/tool/result summaries.
21
21
  5. At stop/session end, hooks append redacted live Q&A and create query or decision candidates when the turn has enough captured context.
22
- 6. When reusable knowledge appears, the active Claude Code/Codex agent folds it into existing durable wiki pages instead of leaving everything as one-off Q&A.
23
- 7. Future sessions start from the improved wiki instead of relying on long chat history.
22
+ 6. At stop/session-end or the next start/prompt after an abrupt shutdown, hooks queue durable cleanup candidates in `outputs/maintenance/queue.md`.
23
+ 7. When reusable knowledge appears, the active Claude Code/Codex agent folds it into existing durable wiki pages instead of leaving everything as one-off Q&A.
24
+ 8. Future sessions start from the improved wiki instead of relying on long chat history.
24
25
 
25
26
  The kit is a template/runtime repository. It must not centralize project wiki contents.
26
27
 
@@ -38,5 +39,6 @@ The maintenance loop is intentionally layered:
38
39
  - `memory.md`: short hot index for current durable facts.
39
40
  - `index.md`: broad navigation map.
40
41
  - MiniSearch + wikilinks: retrieval over the rest of `wiki/**/*.md`, with substring fallback when MiniSearch is not installed in a source checkout.
42
+ - `outputs/maintenance/queue.md`: pending reminders for turns that need durable wiki review.
41
43
  - `lint`: finds broken links, stale pages, duplicates, metadata gaps, secret-like content, and outdated managed rules.
42
44
  - `consolidate`: agent helper that refreshes generated blocks in `memory.md` and `index.md` while preserving handwritten notes and keeping default query/context/session pages out of the durable generated maps.
@@ -40,9 +40,9 @@ when no project `CLAUDE.md` exists. Existing `CLAUDE.md` files are not overwritt
40
40
 
41
41
  The hook records redacted turn summaries but does not deny tool calls only because an input looks sensitive. Hook payloads are stored as small redacted event envelopes rather than full transcripts, and context output is redacted field by field before it is returned to Claude Code.
42
42
 
43
- At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, and any maintenance note for outdated or customized managed rules. At `UserPromptSubmit`, it searches wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set.
43
+ At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, pending queue items, and any maintenance note for outdated or customized managed rules. At `UserPromptSubmit`, it recovers stale turn state, searches wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set plus pending queue reminders.
44
44
 
45
- `PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` records a compaction note, and `PostCompact` records the note and returns fresh wiki context. `SubagentStop`, `Stop`, and `SessionEnd` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like. `Stop` and `SessionEnd` clear the per-session turn buffer after recording; `SubagentStop` does not.
45
+ `PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` records a compaction note, and `PostCompact` records the note and returns fresh wiki context. `SubagentStop`, `Stop`, and `SessionEnd` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like. `Stop` and `SessionEnd` also queue pending maintenance items for durable wiki review, then clear the per-session turn buffer after recording; `SubagentStop` does not.
46
46
 
47
47
  Set `LLM_WIKI_KIT_AUTO_PROJECT_UPDATE=0` only while diagnosing automatic managed-template refresh behavior.
48
48
 
@@ -29,12 +29,12 @@ Handled events:
29
29
 
30
30
  Expected behavior:
31
31
 
32
- - `SessionStart` first attempts a safe managed-template refresh, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, and any maintenance note for outdated or customized managed rules.
33
- - `UserPromptSubmit` searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set.
32
+ - `SessionStart` first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, then injects `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, recent log context, operating rules, pending queue items, and any maintenance note for outdated or customized managed rules.
33
+ - `UserPromptSubmit` recovers stale turn state, searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, and injects the smallest useful context set plus pending queue reminders.
34
34
  - `PreToolUse` records redacted tool summaries without blocking tool calls.
35
35
  - `PostToolUse` records redacted tool summaries in a turn buffer.
36
36
  - `PreCompact` records a compaction note; `PostCompact` records the note and returns fresh wiki context.
37
- - `SubagentStop` and `Stop` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like.
37
+ - `SubagentStop` and `Stop` append live Q&A and create `wiki/queries/` candidates when a captured question exists. A `wiki/decisions/` page is created only when the captured turn looks decision-like. `Stop` also queues a pending maintenance item for durable wiki review.
38
38
  - `Stop` clears the per-session turn buffer after recording. `SubagentStop` leaves the parent turn buffer available for the final stop event.
39
39
 
40
40
  Hook payloads are stored as small redacted event envelopes rather than full transcripts. Context output is also redacted field by field before it is returned to Codex.
@@ -86,6 +86,7 @@ llm-wiki post-update --all --workspace /path/to/search-root
86
86
  llm-wiki context "search phrase" --workspace /path/to/project
87
87
  llm-wiki lint --workspace /path/to/project
88
88
  llm-wiki consolidate --workspace /path/to/project
89
+ llm-wiki maintenance --workspace /path/to/project
89
90
  ```
90
91
 
91
92
  `status` is offline and answers whether the local installation is internally consistent:
@@ -96,6 +97,7 @@ llm-wiki consolidate --workspace /path/to/project
96
97
  - project template state from `llm-wiki/.kit-state.json`
97
98
  - current managed file hashes
98
99
  - auto-updateable managed rules and managed-looking rules that need agent cleanup
100
+ - pending wiki maintenance count
99
101
 
100
102
  `update --check [--to <version-or-tag>]` is online and asks npm for the target version. It reports `update available` only when that registry target is newer than the installed version, so it does not suggest downgrades.
101
103
 
@@ -120,6 +122,8 @@ After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working
120
122
 
121
123
  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 the context automatically, and the active agent is expected to update durable wiki pages when reusable project knowledge appears.
122
124
 
125
+ `Stop` and `SessionEnd` write pending cleanup candidates to `llm-wiki/outputs/maintenance/queue.md` after live Q&A/query/decision capture. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete, then inject up to five pending items into context. This is a recovery and reminder layer, not a full transcript capture path.
126
+
123
127
  `llm-wiki context "<query>"` is the manual debug form of hook context injection. It reads:
124
128
 
125
129
  - `llm-wiki/wiki/memory.md` as the short hot index
@@ -148,6 +152,7 @@ llm-wiki context "auth architecture" --workspace /path/to/project --limit 8 --no
148
152
  - duplicate aliases or titles
149
153
  - stale pages and orphan candidates
150
154
  - outdated managed rules/templates from earlier `llm-wiki-kit` versions
155
+ - stale or oversized maintenance queues
151
156
 
152
157
  Broken links, invalid source IDs, and secret-like content are errors and return exit code 1. Metadata and discoverability gaps are warnings.
153
158
 
@@ -163,6 +168,8 @@ Broken links, invalid source IDs, and secret-like content are errors and return
163
168
 
164
169
  Agents may run `consolidate` after meaningful wiki growth. Users should not need to run it after every turn.
165
170
 
171
+ `llm-wiki maintenance --workspace <project>` prints queue counts and the first pending items. It does not merge wiki pages by itself; the active agent should review pending items, update the closest existing durable wiki document, then mark the queue item `done` or `skipped`.
172
+
166
173
  When a new runtime sees an older project, `SessionStart`/`InstructionsLoaded` automatically reapplies safe managed template updates. Files that are clearly generated by older kit versions are refreshed. Files that look user-edited are preserved and surfaced to the active agent as cleanup context instead of being overwritten. Set `LLM_WIKI_KIT_AUTO_PROJECT_UPDATE=0` only while diagnosing automatic template refresh behavior.
167
174
 
168
175
  ## Updating User-Local Or nvm Installs
@@ -177,6 +177,19 @@ Check:
177
177
  - project `CLAUDE.md` exists or imports `@AGENTS.md`
178
178
  - the session was restarted after install
179
179
 
180
+ ## Maintenance Queue Is Empty Or Stale
181
+
182
+ `llm-wiki/outputs/maintenance/queue.md` is created only after a captured `Stop`/`SessionEnd` turn, or when `SessionStart`/`UserPromptSubmit` recovers stale per-turn state from a session that did not stop cleanly.
183
+
184
+ Check the queue and health warnings:
185
+
186
+ ```bash
187
+ llm-wiki maintenance --workspace /path/to/project
188
+ llm-wiki lint --workspace /path/to/project
189
+ ```
190
+
191
+ If the queue is always empty, confirm hooks run and that the turn had a captured `UserPromptSubmit`. If pending items stay around, the active agent should merge reusable content into existing durable wiki pages and mark each queue item `done` or `skipped`.
192
+
180
193
  ## Authentication Values Were Redacted
181
194
 
182
195
  The hook does not block tool calls only because inputs look sensitive. Durable summaries redact authentication values before writing, while ordinary work context such as dates, phone numbers, emails, and business identifiers is preserved by default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-wiki-kit",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Hook-first living LLM Wiki runtime for Codex and Claude Code.",
5
5
  "type": "module",
6
6
  "files": [
package/src/cli.js CHANGED
@@ -2,6 +2,7 @@ import { resolve } from 'path';
2
2
  import { formatConsolidateResult, runConsolidate } from './consolidate.js';
3
3
  import { handleHook } from './hook.js';
4
4
  import { install, status, uninstall } from './install.js';
5
+ import { formatMaintenanceResult, maintenanceSummary } from './maintenance.js';
5
6
  import { bootstrapProject } from './project.js';
6
7
  import { inspectProjectState } from './project-state.js';
7
8
  import { commandForProject, knownProjectRoots, recordProject } from './projects.js';
@@ -98,6 +99,7 @@ Usage:
98
99
  llm-wiki context "<query>" --workspace <project> [--limit 5] [--no-expand]
99
100
  llm-wiki lint --workspace <project>
100
101
  llm-wiki consolidate --workspace <project> [--dry-run]
102
+ llm-wiki maintenance --workspace <project> [--json]
101
103
  `);
102
104
  return;
103
105
  }
@@ -195,6 +197,12 @@ Usage:
195
197
  return;
196
198
  }
197
199
 
200
+ if (command === 'maintenance') {
201
+ const projectRoot = resolve(options.workspace || process.cwd());
202
+ printJsonOrText(await maintenanceSummary(projectRoot, options), options, formatMaintenanceResult);
203
+ return;
204
+ }
205
+
198
206
  if (command === 'hook') {
199
207
  const provider = rest[0] || 'codex';
200
208
  const eventName = rest[1];
@@ -250,6 +258,7 @@ function formatStatus(value) {
250
258
  `- project templates current: ${project.managedFilesCurrent ? 'yes' : 'no'}`,
251
259
  `- project auto-updateable rules: ${autoUpdateCount}`,
252
260
  `- project rules needing agent cleanup: ${attentionCount}`,
261
+ `- project maintenance pending: ${value.maintenance?.pendingCount ?? 0}`,
253
262
  ].join('\n');
254
263
  }
255
264
 
package/src/constants.js CHANGED
@@ -52,6 +52,7 @@ export const LLM_WIKI_DIRS = [
52
52
  'wiki/queries',
53
53
  'outputs/questions',
54
54
  'outputs/reports',
55
+ 'outputs/maintenance',
55
56
  'procedures',
56
57
  ];
57
58
 
package/src/hook.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { findProjectRoot } from './fs-utils.js';
2
2
  import { bootstrapProject, appendContextNote, appendLiveQa, appendSessionEnvelope, appendWikiLog, buildContextBrief, writeDecisionPage, writeQueryPage } from './project.js';
3
+ import { recoverStaleTurnStates, recordMaintenanceForEntry } from './maintenance.js';
3
4
  import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
4
5
  import { recordProject } from './projects.js';
5
6
  import { summarizeForStorage } from './redaction.js';
@@ -61,6 +62,9 @@ export async function handleHook(provider, explicitEvent) {
61
62
  await recordProject(projectRoot, 'hook').catch(() => {});
62
63
  await autoUpdateManagedProject(projectRoot, eventName).catch(() => {});
63
64
  await appendSessionEnvelope(projectRoot, eventName, payload).catch(() => {});
65
+ if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded' || eventName === 'UserPromptSubmit') {
66
+ await recoverStaleTurnStates(projectRoot).catch(() => []);
67
+ }
64
68
 
65
69
  if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded') {
66
70
  const context = await buildContextBrief(projectRoot, 'SessionStart');
@@ -96,9 +100,16 @@ export async function handleHook(provider, explicitEvent) {
96
100
  const assistantText = payload.last_assistant_message || payload.response || payload.assistant_response || '';
97
101
  const entry = await buildEntryFromState(projectRoot, payload, assistantText);
98
102
  if (entry.question !== '(not captured)' || entry.result !== '(not captured)') {
99
- await appendLiveQa(projectRoot, entry);
103
+ const liveQaPath = await appendLiveQa(projectRoot, entry);
100
104
  const queryPath = await writeQueryPage(projectRoot, entry);
101
105
  const decisionPath = await writeDecisionPage(projectRoot, entry);
106
+ if (eventName === 'Stop' || eventName === 'SessionEnd') {
107
+ await recordMaintenanceForEntry(projectRoot, entry, {
108
+ source: decisionPath || queryPath || liveQaPath,
109
+ eventName,
110
+ reason: 'Captured turn needs durable wiki review.',
111
+ }).catch(() => {});
112
+ }
102
113
  await appendWikiLog(projectRoot, `captured ${eventName}${queryPath ? `; query=${relative(projectRoot, queryPath)}` : ''}${decisionPath ? `; decision=${relative(projectRoot, decisionPath)}` : ''}`);
103
114
  }
104
115
  if (eventName === 'Stop' || eventName === 'SessionEnd') {
package/src/install.js CHANGED
@@ -4,6 +4,7 @@ import { spawnSync } from 'child_process';
4
4
  import { join, resolve } from 'path';
5
5
  import { CLAUDE_EVENTS, CODEX_EVENTS, KIT_NAME } from './constants.js';
6
6
  import { backupFile, exists, homeDir, readJson, safeSymlink, writeJson } from './fs-utils.js';
7
+ import { maintenanceSummary } from './maintenance.js';
7
8
  import { inspectProjectState } from './project-state.js';
8
9
  import { bootstrapProject } from './project.js';
9
10
  import { recordProject } from './projects.js';
@@ -121,7 +122,10 @@ async function reconcileLocalBin(localBinPath) {
121
122
 
122
123
  function addHook(hooks, eventName, command, options = {}) {
123
124
  hooks[eventName] = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
124
- const already = JSON.stringify(hooks[eventName]).includes(command);
125
+ const already = hooks[eventName].some((entry) => (
126
+ Array.isArray(entry?.hooks) &&
127
+ entry.hooks.some((hook) => hook?.type === 'command' && hook?.command === command)
128
+ ));
125
129
  if (already) return false;
126
130
  const entry = {
127
131
  hooks: [
@@ -280,5 +284,6 @@ export async function status(options = {}) {
280
284
  codexHooksPath,
281
285
  claudeSettingsPath,
282
286
  project: await inspectProjectState(workspace),
287
+ maintenance: await maintenanceSummary(workspace),
283
288
  };
284
289
  }
@@ -0,0 +1,253 @@
1
+ import { readdir, unlink } from 'fs/promises';
2
+ import { join, relative } from 'path';
3
+ import { appendText, exists, kitDataDir, readJson, readText, sha256, writeTextIfMissing } from './fs-utils.js';
4
+ import { redactText, summarizeForStorage } from './redaction.js';
5
+ import { buildEntryFromTurnState, hasRecoverableTurnState } from './state.js';
6
+
7
+ export const MAINTENANCE_QUEUE_REL = 'llm-wiki/outputs/maintenance/queue.md';
8
+ const DEFAULT_STALE_TURN_MS = 10 * 60 * 1000;
9
+ const DEFAULT_STALE_PENDING_DAYS = 7;
10
+ const DEFAULT_PENDING_LIMIT = 20;
11
+
12
+ function queuePath(projectRoot) {
13
+ return join(projectRoot, MAINTENANCE_QUEUE_REL);
14
+ }
15
+
16
+ function queueHeader() {
17
+ return [
18
+ '# LLM Wiki Maintenance Queue',
19
+ '',
20
+ '정식 wiki 문서에 합칠 후보를 보관한다. Hook은 후보만 만들고, agent가 기존 durable wiki 문서를 찾아 병합한다.',
21
+ '',
22
+ 'Status values: pending, done, skipped.',
23
+ '',
24
+ ].join('\n');
25
+ }
26
+
27
+ function nowIso() {
28
+ return new Date().toISOString();
29
+ }
30
+
31
+ function staleTurnMs() {
32
+ const value = Number(process.env.LLM_WIKI_KIT_STALE_TURN_MS || DEFAULT_STALE_TURN_MS);
33
+ return Number.isFinite(value) && value >= 0 ? value : DEFAULT_STALE_TURN_MS;
34
+ }
35
+
36
+ function normalizeSource(projectRoot, source) {
37
+ if (!source) return '';
38
+ const text = String(source);
39
+ if (text.startsWith('state:')) return text;
40
+ if (text.startsWith(projectRoot)) return relative(projectRoot, text).split('\\').join('/');
41
+ return text.split('\\').join('/');
42
+ }
43
+
44
+ function relativeSource(projectRoot, path) {
45
+ return normalizeSource(projectRoot, path);
46
+ }
47
+
48
+ function sanitizeField(value, maxLength = 500) {
49
+ return summarizeForStorage(String(value || '').replace(/\s+/g, ' ').trim(), maxLength);
50
+ }
51
+
52
+ function inferSuggestedTarget(entry, source) {
53
+ const text = `${entry?.question || ''}\n${entry?.result || ''}\n${entry?.work || ''}\n${source || ''}`.toLowerCase();
54
+ if (String(source || '').includes('wiki/decisions/') || /decision|decided|결정|선택|채택|확정/.test(text)) return 'wiki/decisions/';
55
+ if (/error|bug|fix|debug|failure|failed|실패|오류|버그|수정|원인/.test(text)) return 'wiki/debugging/';
56
+ if (/architecture|flow|runtime|hook|update|install|context|memory|구조|아키텍처|흐름|설치|업데이트/.test(text)) return 'wiki/architecture/';
57
+ if (/procedure|process|policy|rule|운영|절차|규칙|정책/.test(text)) return 'procedures/';
58
+ return 'wiki/concepts/';
59
+ }
60
+
61
+ function itemId(projectRoot, source, entry) {
62
+ const question = entry?.question || entry?.topic || '';
63
+ const firstInput = entry?.firstTimestamp || entry?.createdAt || source || '';
64
+ return sha256(`${projectRoot}\n${source}\n${question}\n${firstInput}`).slice(0, 24);
65
+ }
66
+
67
+ function itemBlock(item) {
68
+ return [
69
+ `## ${item.status || 'pending'} - ${item.topic || 'maintenance item'}`,
70
+ '',
71
+ `- id: ${item.id}`,
72
+ `- created_at: ${item.created_at}`,
73
+ `- last_seen_at: ${item.last_seen_at}`,
74
+ `- source: ${item.source}`,
75
+ `- suggested_target: ${item.suggested_target}`,
76
+ `- reason: ${item.reason}`,
77
+ `- result_missing: ${item.result_missing ? 'true' : 'false'}`,
78
+ '',
79
+ ].join('\n');
80
+ }
81
+
82
+ export async function readMaintenanceQueue(projectRoot) {
83
+ const path = queuePath(projectRoot);
84
+ const text = await readText(path, '');
85
+ const items = [];
86
+ let current = null;
87
+
88
+ const pushCurrent = () => {
89
+ if (current) items.push(current);
90
+ current = null;
91
+ };
92
+
93
+ for (const line of text.split(/\r?\n/)) {
94
+ const header = line.match(/^##\s+(pending|done|skipped)\s+-\s*(.*)$/);
95
+ if (header) {
96
+ pushCurrent();
97
+ current = {
98
+ status: header[1],
99
+ topic: header[2].trim(),
100
+ fields: {},
101
+ };
102
+ continue;
103
+ }
104
+ if (!current) continue;
105
+ const field = line.match(/^-\s+([a-z_]+):\s*(.*)$/);
106
+ if (field) {
107
+ current.fields[field[1]] = field[2].trim();
108
+ }
109
+ }
110
+ pushCurrent();
111
+
112
+ return {
113
+ path,
114
+ exists: await exists(path),
115
+ items: items.map((item) => ({
116
+ ...item.fields,
117
+ status: item.status,
118
+ topic: item.topic,
119
+ result_missing: String(item.fields.result_missing || '').toLowerCase() === 'true',
120
+ })),
121
+ };
122
+ }
123
+
124
+ export function summarizeMaintenanceQueue(queue, options = {}) {
125
+ const staleDays = options.staleDays ?? DEFAULT_STALE_PENDING_DAYS;
126
+ const pendingLimit = options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
127
+ const pending = queue.items.filter((item) => item.status === 'pending');
128
+ const staleCutoff = Date.now() - staleDays * 24 * 60 * 60 * 1000;
129
+ const stalePending = pending.filter((item) => {
130
+ const time = Date.parse(item.created_at || item.last_seen_at || '');
131
+ return Number.isFinite(time) && time < staleCutoff;
132
+ });
133
+ return {
134
+ path: queue.path,
135
+ exists: queue.exists,
136
+ items: queue.items,
137
+ pending,
138
+ done: queue.items.filter((item) => item.status === 'done'),
139
+ skipped: queue.items.filter((item) => item.status === 'skipped'),
140
+ pendingCount: pending.length,
141
+ doneCount: queue.items.filter((item) => item.status === 'done').length,
142
+ skippedCount: queue.items.filter((item) => item.status === 'skipped').length,
143
+ stalePending,
144
+ stalePendingDays: staleDays,
145
+ pendingLimit,
146
+ tooManyPending: pending.length > pendingLimit,
147
+ };
148
+ }
149
+
150
+ export async function maintenanceSummary(projectRoot, options = {}) {
151
+ return summarizeMaintenanceQueue(await readMaintenanceQueue(projectRoot), options);
152
+ }
153
+
154
+ export async function appendMaintenanceItem(projectRoot, item) {
155
+ const path = queuePath(projectRoot);
156
+ await writeTextIfMissing(path, queueHeader());
157
+ const queue = await readMaintenanceQueue(projectRoot);
158
+ if (queue.items.some((existing) => existing.id === item.id)) {
159
+ return { created: false, path, id: item.id };
160
+ }
161
+ await appendText(path, redactText(`\n${itemBlock(item)}`, 6000));
162
+ return { created: true, path, id: item.id };
163
+ }
164
+
165
+ export async function recordMaintenanceForEntry(projectRoot, entry, options = {}) {
166
+ const source = relativeSource(projectRoot, options.source || options.queryPath || options.decisionPath || options.liveQaPath || '');
167
+ if (!source && !options.resultMissing) return { created: false, reason: 'missing-source' };
168
+ if (!entry?.question || entry.question === '(not captured)') return { created: false, reason: 'missing-question' };
169
+ const created = nowIso();
170
+ const item = {
171
+ id: itemId(projectRoot, source, entry),
172
+ status: 'pending',
173
+ topic: sanitizeField(entry.topic || entry.question, 120),
174
+ created_at: created,
175
+ last_seen_at: created,
176
+ source,
177
+ suggested_target: options.suggestedTarget || inferSuggestedTarget(entry, source),
178
+ reason: sanitizeField(options.reason || `Captured ${options.eventName || 'turn'} needs durable wiki review.`, 300),
179
+ result_missing: Boolean(options.resultMissing),
180
+ };
181
+ return appendMaintenanceItem(projectRoot, item);
182
+ }
183
+
184
+ function stateDir() {
185
+ return join(kitDataDir(), 'state');
186
+ }
187
+
188
+ async function stateFiles() {
189
+ try {
190
+ return (await readdir(stateDir()))
191
+ .filter((file) => file.endsWith('.json'))
192
+ .map((file) => join(stateDir(), file));
193
+ } catch {
194
+ return [];
195
+ }
196
+ }
197
+
198
+ export async function recoverStaleTurnStates(projectRoot, options = {}) {
199
+ const output = [];
200
+ const cutoffMs = options.staleMs ?? staleTurnMs();
201
+ const now = Date.now();
202
+ for (const path of await stateFiles()) {
203
+ const state = await readJson(path, null);
204
+ if (!state || state.projectRoot !== projectRoot) continue;
205
+ const updated = Date.parse(state.updated_at || '');
206
+ if (Number.isFinite(updated) && now - updated < cutoffMs) continue;
207
+ if (!hasRecoverableTurnState(state)) continue;
208
+ const entry = buildEntryFromTurnState(state, '');
209
+ const result = await recordMaintenanceForEntry(projectRoot, entry, {
210
+ source: `state:${state.session || path.split('/').pop()?.replace(/\.json$/, '')}`,
211
+ eventName: 'recovered stale turn state',
212
+ reason: 'Recovered from stale turn state because Stop/SessionEnd did not complete.',
213
+ resultMissing: true,
214
+ });
215
+ await unlink(path).catch(() => {});
216
+ output.push({ ...result, statePath: path, session: state.session });
217
+ }
218
+ return output;
219
+ }
220
+
221
+ export function formatMaintenanceContext(summary, options = {}) {
222
+ const limit = options.limit || 5;
223
+ const pending = summary.pending.slice(0, limit);
224
+ if (pending.length === 0) return '';
225
+ const lines = [
226
+ 'LLM Wiki maintenance queue:',
227
+ '- Pending items should be merged into existing durable wiki pages when relevant; update queue status to done or skipped after review.',
228
+ ];
229
+ for (const item of pending) {
230
+ lines.push(`- ${item.topic || item.id}: source=${item.source}; target=${item.suggested_target}; reason=${item.reason}${item.result_missing ? '; result_missing=true' : ''}`);
231
+ }
232
+ if (summary.pending.length > pending.length) {
233
+ lines.push(`- ${summary.pending.length - pending.length} more pending item(s) hidden from context.`);
234
+ }
235
+ return lines.join('\n');
236
+ }
237
+
238
+ export function formatMaintenanceResult(summary) {
239
+ const lines = [
240
+ 'llm-wiki maintenance',
241
+ `- queue: ${summary.path}`,
242
+ `- pending: ${summary.pendingCount}`,
243
+ `- done: ${summary.doneCount}`,
244
+ `- skipped: ${summary.skippedCount}`,
245
+ ];
246
+ if (summary.pending.length > 0) {
247
+ lines.push('', 'Pending:');
248
+ for (const item of summary.pending.slice(0, 10)) {
249
+ lines.push(`- ${item.topic || item.id}: ${item.source} -> ${item.suggested_target}`);
250
+ }
251
+ }
252
+ return lines.join('\n');
253
+ }
package/src/project.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  writeTextIfMissing,
10
10
  } from './fs-utils.js';
11
11
  import { LLM_WIKI_DIRS } from './constants.js';
12
+ import { formatMaintenanceContext, maintenanceSummary } from './maintenance.js';
12
13
  import { normalizeForStorage, redactText, summarizeForStorage } from './redaction.js';
13
14
  import { gitignore, indexPage, llmWikiAgents, logPage, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
14
15
  import { formatProjectMaintenanceContext, inspectProjectState, recordManagedTemplates } from './project-state.js';
@@ -121,6 +122,7 @@ export async function appendLiveQa(projectRoot, entry) {
121
122
  '',
122
123
  ].join('\n');
123
124
  await appendText(path, redactText(block, 12000));
125
+ return path;
124
126
  }
125
127
 
126
128
  export async function writeQueryPage(projectRoot, entry) {
@@ -166,5 +168,8 @@ export async function buildContextBrief(projectRoot, eventName, query = '') {
166
168
  const maintenance = await inspectProjectState(projectRoot)
167
169
  .then(formatProjectMaintenanceContext)
168
170
  .catch(() => '');
169
- return [formatContextPack(pack), maintenance].filter(Boolean).join('\n\n');
171
+ const wikiMaintenance = await maintenanceSummary(projectRoot)
172
+ .then(formatMaintenanceContext)
173
+ .catch(() => '');
174
+ return [formatContextPack(pack), maintenance, wikiMaintenance].filter(Boolean).join('\n\n');
170
175
  }
package/src/state.js CHANGED
@@ -63,16 +63,38 @@ export async function rememberTool(projectRoot, payload, summary) {
63
63
 
64
64
  export async function buildEntryFromState(projectRoot, payload, assistantText) {
65
65
  const state = await readTurnState(projectRoot, payload);
66
- const question = state.questions.at(-1)?.text || summarizeForStorage(payload.prompt || payload.user_prompt || '', 2000);
67
- const result = summarizeForStorage(assistantText || payload.last_assistant_message || payload.response || '', 4000);
66
+ return buildEntryFromTurnState(state, assistantText || payload.last_assistant_message || payload.response || '');
67
+ }
68
+
69
+ export function hasRecoverableTurnState(state) {
70
+ return Boolean(
71
+ state &&
72
+ (
73
+ (Array.isArray(state.questions) && state.questions.length > 0) ||
74
+ (Array.isArray(state.tools) && state.tools.length > 0) ||
75
+ (Array.isArray(state.files) && state.files.length > 0) ||
76
+ (Array.isArray(state.verification) && state.verification.length > 0)
77
+ )
78
+ );
79
+ }
80
+
81
+ export function buildEntryFromTurnState(state, assistantText = '') {
82
+ const questions = Array.isArray(state.questions) ? state.questions : [];
83
+ const tools = Array.isArray(state.tools) ? state.tools : [];
84
+ const files = Array.isArray(state.files) ? state.files : [];
85
+ const verification = Array.isArray(state.verification) ? state.verification : [];
86
+ const question = questions.at(-1)?.text || '';
87
+ const result = summarizeForStorage(assistantText || '', 4000);
68
88
  const topic = question ? question.replace(/\s+/g, ' ').slice(0, 80) : 'session turn';
69
89
  return {
70
90
  topic,
71
91
  question: question || '(not captured)',
72
- work: state.tools.map((item) => `- ${item.text}`).join('\n') || '(not captured)',
92
+ work: tools.map((item) => `- ${item.text}`).join('\n') || '(not captured)',
73
93
  result: result || '(not captured)',
74
- changedFiles: [...new Set(state.files)].map((item) => `- ${item}`).join('\n') || '(not captured)',
75
- verification: state.verification.map((item) => `- ${item}`).join('\n') || '(not captured)',
94
+ changedFiles: [...new Set(files)].map((item) => `- ${item}`).join('\n') || '(not captured)',
95
+ verification: verification.map((item) => `- ${item}`).join('\n') || '(not captured)',
76
96
  followUp: '',
97
+ firstTimestamp: questions[0]?.at || tools[0]?.at || state.updated_at,
98
+ session: state.session,
77
99
  };
78
100
  }
package/src/templates.js CHANGED
@@ -15,6 +15,7 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
15
15
  - \`llm-wiki/wiki/memory.md\`는 짧은 핵심 기억이다. 긴 설명 대신 현재 상태와 중요한 문서 링크만 유지한다.
16
16
  - hook이 주입한 context를 우선 사용한다. 수동 확인이나 정리에는 \`llm-wiki context\`, \`llm-wiki lint\`, \`llm-wiki consolidate\`를 agent 보조 도구로 사용한다.
17
17
  - hook은 redacted live Q&A와 query/decision 후보를 기록한다. 재사용 가능한 지식은 agent가 기존 정식 wiki 문서에 합친다.
18
+ - hook은 종료/시작 경계에서 정리 후보를 \`llm-wiki/outputs/maintenance/queue.md\`에 남길 수 있다. pending 항목은 agent가 기존 정식 wiki 문서에 병합한 뒤 done 또는 skipped로 표시한다.
18
19
  - 새 문서를 만들기 전에 기존 wiki 문서를 먼저 찾아 갱신한다. 반복해서 쓸 사실은 \`outputs/questions/\`에만 두지 말고 적절한 wiki 문서에 합친다.
19
20
  - 일회성 작업 기록은 \`llm-wiki/outputs/questions/\`에 보존하고, 재사용 가능한 결론은 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 반영한다.
20
21
  - 검증 명령, 근거 파일, 불확실한 점을 함께 남긴다. 추론은 추론이라고 표시하고, 모순은 지우지 말고 Open Questions 또는 Contradictions에 남긴다.
@@ -47,6 +48,7 @@ Codex와 Claude Code를 평소처럼 사용하는 동안 living Markdown LLM Wik
47
48
  - 중요한 주장에는 \`source_ids\`, 파일 경로, 검증 명령 중 하나 이상을 남긴다.
48
49
  - 오래 남길 내용이 생기면 새 문서부터 만들지 말고 기존 \`wiki/\` 문서를 먼저 찾아 갱신한다.
49
50
  - 반복해서 쓸 지식은 \`outputs/questions/\`에만 두지 말고 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
51
+ - hook이 만든 \`outputs/maintenance/queue.md\` pending 항목은 다음 작업 시작 시 확인하고, 기존 정식 wiki 문서에 병합한 뒤 done 또는 skipped로 표시한다.
50
52
  - \`wiki/memory.md\`는 짧게 유지한다. 긴 설명 대신 현재 상태와 중요한 문서 링크를 둔다.
51
53
  - 모순은 덮어쓰지 말고 \`Contradictions\` 또는 \`Open Questions\`에 보존한다.
52
54
  - 인증값, token, password, private key, \`.env\` 원문은 wiki에 저장하지 않는다.
@@ -75,6 +77,7 @@ superseded_by: []
75
77
  - query: hook이 주입한 context를 우선 사용한다. 수동 명령은 점검용이며 일반 사용 흐름의 필수 단계가 아니다.
76
78
  - lint: agent가 필요할 때 wiki 건강 상태를 점검하는 보조 도구다. 사용자가 매번 직접 실행해야 하는 명령이 아니다.
77
79
  - consolidate: agent가 \`memory.md\`/\`index.md\` generated block을 안전하게 갱신할 때 쓰는 보조 도구다. 손글씨 영역과 정식 문서 본문은 덮어쓰지 않는다.
80
+ - maintenance: \`outputs/maintenance/queue.md\`는 종료/시작 경계에서 생긴 정리 후보 목록이다. agent가 기존 문서 병합 여부를 판단하고 상태를 갱신한다.
78
81
  `;
79
82
  }
80
83
 
package/src/wiki-lint.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readdir } from 'fs/promises';
2
2
  import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'path';
3
3
  import { exists, readText } from './fs-utils.js';
4
+ import { maintenanceSummary } from './maintenance.js';
4
5
  import { inspectProjectState } from './project-state.js';
5
6
  import { hasSecretLikeText } from './redaction.js';
6
7
  import {
@@ -244,6 +245,14 @@ export async function runLint(projectRoot, options = {}) {
244
245
  }
245
246
  }
246
247
 
248
+ const maintenance = await maintenanceSummary(projectRoot).catch(() => null);
249
+ if (maintenance?.tooManyPending) {
250
+ issues.push(issue('warning', 'maintenance-too-many-pending', 'outputs/maintenance/queue.md', `maintenance queue has ${maintenance.pendingCount} pending items; review and merge durable items into wiki pages`));
251
+ }
252
+ if ((maintenance?.stalePending || []).length > 0) {
253
+ issues.push(issue('warning', 'maintenance-stale-pending', 'outputs/maintenance/queue.md', `${maintenance.stalePending.length} pending maintenance item(s) are older than ${maintenance.stalePendingDays} days`));
254
+ }
255
+
247
256
  return lintResult(projectRoot, pages, issues);
248
257
  }
249
258