llm-wiki-kit 0.2.8 → 0.2.10

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.
@@ -5,6 +5,14 @@ import { hasSecretLikeText, isSensitivePath, normalizeForStorage } from './redac
5
5
  import { indexPage, memoryPage } from './templates.js';
6
6
  import { collectWikiPages } from './wiki-model.js';
7
7
  import { runLint } from './wiki-lint.js';
8
+ import {
9
+ isArchivedPage,
10
+ isEpisodicLayerPage,
11
+ isPromotedDurablePage,
12
+ isStalePage,
13
+ isSupersededPage,
14
+ pageImportance,
15
+ } from './wiki-visibility.js';
8
16
 
9
17
  export const MEMORY_START = '<!-- llm-wiki-kit:memory-start -->';
10
18
  export const MEMORY_END = '<!-- llm-wiki-kit:memory-end -->';
@@ -37,37 +45,53 @@ function pageReference(page) {
37
45
  return `[[${pageTarget(page)}|${page.title}]]`;
38
46
  }
39
47
 
40
- function importance(page) {
41
- const value = Number(page.frontmatter.importance || 0);
42
- return Number.isFinite(value) ? value : 0;
43
- }
44
-
45
48
  function isPromotableEpisodicPage(page) {
46
- return ['semantic', 'procedural'].includes(page.memoryType) && importance(page) >= 4;
49
+ return isPromotedDurablePage(page);
47
50
  }
48
51
 
49
- function isGeneratedBlockCandidate(page) {
50
- if (['wiki/index.md', 'wiki/log.md', 'wiki/memory.md'].includes(page.rel)) return false;
51
- if (isSensitivePath(page.rel) || isSensitivePath(page.title)) return false;
52
- if (hasSecretLikeText(`${page.rel}\n${page.title}\n${page.content}`)) return false;
53
- const type = String(page.type || '').toLowerCase();
54
- if (
55
- type === 'query' ||
56
- type === 'context' ||
57
- type === 'session-log' ||
58
- page.rel.startsWith('wiki/queries/') ||
59
- page.rel.startsWith('wiki/context/')
60
- ) {
61
- return isPromotableEpisodicPage(page);
52
+ function classifyGeneratedPages(pages) {
53
+ const stats = {
54
+ durablePages: 0,
55
+ memoryPages: 0,
56
+ hiddenEpisodicPages: 0,
57
+ archivedSkippedPages: 0,
58
+ staleSkippedPages: 0,
59
+ supersededSkippedPages: 0,
60
+ sensitiveSkippedPages: 0,
61
+ };
62
+ const candidates = [];
63
+ for (const page of pages) {
64
+ if (['wiki/index.md', 'wiki/log.md', 'wiki/memory.md'].includes(page.rel)) continue;
65
+ if (isSensitivePath(page.rel) || isSensitivePath(page.title) || hasSecretLikeText(`${page.rel}\n${page.title}\n${page.content}`)) {
66
+ stats.sensitiveSkippedPages += 1;
67
+ continue;
68
+ }
69
+ if (isArchivedPage(page)) {
70
+ stats.archivedSkippedPages += 1;
71
+ continue;
72
+ }
73
+ if (isSupersededPage(page)) {
74
+ stats.supersededSkippedPages += 1;
75
+ continue;
76
+ }
77
+ if (isStalePage(page)) {
78
+ stats.staleSkippedPages += 1;
79
+ continue;
80
+ }
81
+ if (isEpisodicLayerPage(page) && !isPromotableEpisodicPage(page)) {
82
+ stats.hiddenEpisodicPages += 1;
83
+ continue;
84
+ }
85
+ candidates.push(page);
62
86
  }
63
- return true;
87
+ stats.durablePages = candidates.length;
88
+ return { candidates, stats };
64
89
  }
65
90
 
66
91
  function sortedPages(pages) {
67
92
  return pages
68
- .filter(isGeneratedBlockCandidate)
69
93
  .sort((a, b) => {
70
- const importanceDiff = importance(b) - importance(a);
94
+ const importanceDiff = pageImportance(b) - pageImportance(a);
71
95
  if (importanceDiff !== 0) return importanceDiff;
72
96
  const typeDiff = String(a.type || '').localeCompare(String(b.type || ''));
73
97
  if (typeDiff !== 0) return typeDiff;
@@ -75,12 +99,16 @@ function sortedPages(pages) {
75
99
  });
76
100
  }
77
101
 
78
- function buildGeneratedMemoryBlock(pages) {
79
- const candidates = sortedPages(pages).slice(0, 25);
102
+ function buildGeneratedMemoryBlock(pages, stats) {
103
+ const allCandidates = sortedPages(pages);
104
+ const candidates = allCandidates.slice(0, 25);
105
+ stats.memoryPages = candidates.length;
80
106
  const lines = [
81
107
  '## Generated Memory Map',
82
108
  '',
83
- `- Pages indexed: ${sortedPages(pages).length}`,
109
+ `- Durable pages indexed: ${allCandidates.length}`,
110
+ `- Hidden episodic pages: ${stats.hiddenEpisodicPages}`,
111
+ `- Skipped archived/stale/superseded pages: ${stats.archivedSkippedPages}/${stats.staleSkippedPages}/${stats.supersededSkippedPages}`,
84
112
  ];
85
113
  if (candidates.length === 0) {
86
114
  lines.push('- No durable pages found yet.');
@@ -91,7 +119,7 @@ function buildGeneratedMemoryBlock(pages) {
91
119
  const metadata = [
92
120
  page.type || 'unknown',
93
121
  page.memoryType ? `memory:${page.memoryType}` : '',
94
- importance(page) > 0 ? `importance:${importance(page)}` : '',
122
+ pageImportance(page) > 0 ? `importance:${pageImportance(page)}` : '',
95
123
  page.confidence ? `confidence:${page.confidence}` : '',
96
124
  ].filter(Boolean).join(', ');
97
125
  lines.push(`- ${pageReference(page)}${metadata ? ` - ${metadata}` : ''}`);
@@ -111,7 +139,7 @@ function buildGeneratedIndexBlock(pages) {
111
139
  const lines = [
112
140
  '## Generated Page Map',
113
141
  '',
114
- `- Pages indexed: ${candidates.length}`,
142
+ `- Durable pages indexed: ${candidates.length}`,
115
143
  ];
116
144
  if (groups.size === 0) {
117
145
  lines.push('- No durable pages found yet.');
@@ -142,6 +170,7 @@ async function updateMarkedFile(projectRoot, rel, initialContent, startMarker, e
142
170
  export async function runConsolidate(projectRoot, options = {}) {
143
171
  const lintResult = await runLint(projectRoot, { maxFiles: options.maxFiles || 1000 });
144
172
  const pages = await collectWikiPages(projectRoot, { maxFiles: options.maxFiles || 1000 });
173
+ const { candidates, stats } = classifyGeneratedPages(pages);
145
174
  const changed = [];
146
175
  const skipped = [];
147
176
 
@@ -151,7 +180,7 @@ export async function runConsolidate(projectRoot, options = {}) {
151
180
  memoryPage(),
152
181
  MEMORY_START,
153
182
  MEMORY_END,
154
- buildGeneratedMemoryBlock(pages),
183
+ buildGeneratedMemoryBlock(candidates, stats),
155
184
  options,
156
185
  );
157
186
  if (memoryChange?.changed) changed.push(memoryChange.changed);
@@ -163,7 +192,7 @@ export async function runConsolidate(projectRoot, options = {}) {
163
192
  indexPage(),
164
193
  INDEX_START,
165
194
  INDEX_END,
166
- buildGeneratedIndexBlock(pages),
195
+ buildGeneratedIndexBlock(candidates),
167
196
  options,
168
197
  );
169
198
  if (indexChange?.changed) changed.push(indexChange.changed);
@@ -182,6 +211,7 @@ export async function runConsolidate(projectRoot, options = {}) {
182
211
  dryRun: Boolean(options.dryRun),
183
212
  changed,
184
213
  skipped,
214
+ counts: stats,
185
215
  lint: {
186
216
  ok: finalLintResult.ok,
187
217
  errorCount: finalLintResult.errorCount,
@@ -197,6 +227,9 @@ export function formatConsolidateResult(result) {
197
227
  `- dry run: ${result.dryRun ? 'yes' : 'no'}`,
198
228
  `- changed: ${result.changed.length ? result.changed.map((item) => item.path).join(', ') : 'none'}`,
199
229
  `- skipped: ${result.skipped?.length ? result.skipped.map((item) => `${item.path} (${item.reason})`).join(', ') : 'none'}`,
230
+ `- durable pages indexed: ${result.counts?.durablePages ?? 0}`,
231
+ `- hidden episodic pages: ${result.counts?.hiddenEpisodicPages ?? 0}`,
232
+ `- skipped archived/stale/superseded: ${result.counts?.archivedSkippedPages ?? 0}/${result.counts?.staleSkippedPages ?? 0}/${result.counts?.supersededSkippedPages ?? 0}`,
200
233
  `- lint errors: ${result.lint.errorCount}`,
201
234
  `- lint warnings: ${result.lint.warningCount}`,
202
235
  ].join('\n');
package/src/hook.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { findProjectRoot } from './fs-utils.js';
2
2
  import { classifyTurn, formatDurableCaptureGuidance, hasDetectedDurableWikiChange, isLegacyEagerCaptureMode } from './capture-policy.js';
3
3
  import { bootstrapProject, appendLiveQa, appendSessionEnvelope, appendWikiLog, buildContextBrief, writeDecisionPage, writeQueryPage } from './project.js';
4
- import { compactSummaryText, handlePreCompactCapture, recordPostCompactSummary } from './compact-capture.js';
4
+ import { consumeCompactRecoveryContext, finalizeCompactRecovery, handlePreCompactCapture, recordPostCompactSummary } from './compact-capture.js';
5
5
  import { recoverStaleTurnStates, recordMaintenanceForEntry } from './maintenance.js';
6
6
  import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
7
7
  import { recordProject } from './projects.js';
@@ -35,8 +35,16 @@ function toolSummary(payload) {
35
35
  return `${toolName}: ${summarizeForStorage(input, 1200)}`;
36
36
  }
37
37
 
38
- function contextOutput(eventName, context) {
38
+ function supportsAdditionalContext(provider, eventName) {
39
+ const normalized = String(provider || '').toLowerCase();
40
+ if (normalized === 'codex') return ['SessionStart', 'UserPromptSubmit'].includes(eventName);
41
+ if (normalized === 'claude') return ['SessionStart', 'InstructionsLoaded', 'UserPromptSubmit'].includes(eventName);
42
+ return ['SessionStart', 'InstructionsLoaded', 'UserPromptSubmit'].includes(eventName);
43
+ }
44
+
45
+ function contextOutput(provider, eventName, context) {
39
46
  if (!context) return {};
47
+ if (!supportsAdditionalContext(provider, eventName)) return {};
40
48
  return {
41
49
  hookSpecificOutput: {
42
50
  hookEventName: eventName,
@@ -129,21 +137,30 @@ export async function handleHook(provider, explicitEvent) {
129
137
  }
130
138
 
131
139
  if (eventName === 'SessionStart' || eventName === 'InstructionsLoaded') {
132
- const context = await hookContext(projectRoot, eventName, await buildContextBrief(projectRoot, 'SessionStart'), payload);
133
- return contextOutput(eventName, context);
140
+ const recovery = supportsAdditionalContext(provider, eventName)
141
+ ? await consumeCompactRecoveryContext(projectRoot, payload).catch(() => '')
142
+ : '';
143
+ const context = await hookContext(
144
+ projectRoot,
145
+ eventName,
146
+ [recovery, await buildContextBrief(projectRoot, 'SessionStart')].filter(Boolean).join('\n\n'),
147
+ payload
148
+ );
149
+ return contextOutput(provider, eventName, context);
134
150
  }
135
151
 
136
152
  if (eventName === 'UserPromptSubmit') {
137
153
  const prompt = promptText(payload);
138
154
  await rememberQuestion(projectRoot, payload, prompt);
139
155
  const guidance = formatDurableCaptureGuidance(prompt);
156
+ const recovery = await consumeCompactRecoveryContext(projectRoot, payload).catch(() => '');
140
157
  const context = await hookContext(
141
158
  projectRoot,
142
159
  eventName,
143
- [await buildContextBrief(projectRoot, eventName, prompt), guidance].filter(Boolean).join('\n\n'),
160
+ [recovery, await buildContextBrief(projectRoot, eventName, prompt), guidance].filter(Boolean).join('\n\n'),
144
161
  payload
145
162
  );
146
- return contextOutput(eventName, context);
163
+ return contextOutput(provider, eventName, context);
147
164
  }
148
165
 
149
166
  if (eventName === 'PreToolUse') {
@@ -158,10 +175,9 @@ export async function handleHook(provider, explicitEvent) {
158
175
 
159
176
  if (eventName === 'PreCompact' || eventName === 'PostCompact') {
160
177
  if (eventName === 'PostCompact') {
161
- const summary = compactSummaryText(payload);
162
178
  await recordPostCompactSummary(projectRoot, eventName, payload);
163
- const context = await hookContext(projectRoot, eventName, await buildContextBrief(projectRoot, eventName, summary), payload);
164
- return contextOutput(eventName, context);
179
+ await finalizeCompactRecovery(projectRoot, payload).catch(() => {});
180
+ return {};
165
181
  }
166
182
  return handlePreCompactCapture(projectRoot, provider, eventName, payload);
167
183
  }
@@ -4,11 +4,15 @@ import { appendText, exists, kitDataDir, readJson, readText, sha256, writeTextIf
4
4
  import { classifyTurn, isMaintenanceRelatedQuery } from './capture-policy.js';
5
5
  import { redactText, summarizeForStorage } from './redaction.js';
6
6
  import { buildEntryFromTurnState, hasRecoverableTurnState } from './state.js';
7
+ import { collectWikiPages, DEFAULT_MAX_WIKI_FILES, MEMORY_BYTE_LIMIT } from './wiki-model.js';
7
8
 
8
9
  export const MAINTENANCE_QUEUE_REL = 'llm-wiki/outputs/maintenance/queue.md';
9
10
  const DEFAULT_STALE_TURN_MS = 10 * 60 * 1000;
10
11
  const DEFAULT_STALE_PENDING_DAYS = 7;
11
12
  const DEFAULT_PENDING_LIMIT = 20;
13
+ const DEFAULT_REVIEW_PENDING_LIMIT = 5;
14
+ const DEFAULT_REVIEW_INTERVAL_DAYS = 14;
15
+ const MEMORY_NEAR_BUDGET_BYTES = 20 * 1024;
12
16
 
13
17
  function queuePath(projectRoot) {
14
18
  return join(projectRoot, MAINTENANCE_QUEUE_REL);
@@ -125,12 +129,26 @@ export async function readMaintenanceQueue(projectRoot) {
125
129
  export function summarizeMaintenanceQueue(queue, options = {}) {
126
130
  const staleDays = options.staleDays ?? DEFAULT_STALE_PENDING_DAYS;
127
131
  const pendingLimit = options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
132
+ const reviewPendingLimit = options.reviewPendingLimit ?? DEFAULT_REVIEW_PENDING_LIMIT;
133
+ const reviewIntervalDays = options.reviewIntervalDays ?? DEFAULT_REVIEW_INTERVAL_DAYS;
128
134
  const pending = queue.items.filter((item) => item.status === 'pending');
129
135
  const staleCutoff = Date.now() - staleDays * 24 * 60 * 60 * 1000;
130
136
  const stalePending = pending.filter((item) => {
131
137
  const time = Date.parse(item.created_at || item.last_seen_at || '');
132
138
  return Number.isFinite(time) && time < staleCutoff;
133
139
  });
140
+ const reviewItems = queue.items.filter((item) => item.status === 'done' || item.status === 'skipped');
141
+ const reviewTimes = reviewItems
142
+ .map((item) => Date.parse(item.last_seen_at || item.created_at || ''))
143
+ .filter(Number.isFinite);
144
+ const lastReviewMs = reviewTimes.length > 0 ? Math.max(...reviewTimes) : null;
145
+ const reviewReasons = [];
146
+ if (pending.length >= reviewPendingLimit) reviewReasons.push(`pending queue has ${pending.length} items (threshold ${reviewPendingLimit})`);
147
+ if (stalePending.length > 0) reviewReasons.push(`${stalePending.length} pending item(s) older than ${staleDays} days`);
148
+ if (pending.some((item) => item.result_missing)) reviewReasons.push('pending recovered turn state needs review');
149
+ if (lastReviewMs && Date.now() - lastReviewMs >= reviewIntervalDays * 24 * 60 * 60 * 1000) {
150
+ reviewReasons.push(`last maintenance review is older than ${reviewIntervalDays} days`);
151
+ }
134
152
  return {
135
153
  path: queue.path,
136
154
  exists: queue.exists,
@@ -142,14 +160,79 @@ export function summarizeMaintenanceQueue(queue, options = {}) {
142
160
  doneCount: queue.items.filter((item) => item.status === 'done').length,
143
161
  skippedCount: queue.items.filter((item) => item.status === 'skipped').length,
144
162
  stalePending,
163
+ stalePendingCount: stalePending.length,
145
164
  stalePendingDays: staleDays,
146
165
  pendingLimit,
166
+ reviewPendingLimit,
167
+ reviewIntervalDays,
168
+ lastReviewAt: lastReviewMs ? new Date(lastReviewMs).toISOString() : null,
169
+ reviewDue: reviewReasons.length > 0,
170
+ reviewReasons,
147
171
  tooManyPending: pending.length > pendingLimit,
148
172
  };
149
173
  }
150
174
 
151
175
  export async function maintenanceSummary(projectRoot, options = {}) {
152
- return summarizeMaintenanceQueue(await readMaintenanceQueue(projectRoot), options);
176
+ const summary = summarizeMaintenanceQueue(await readMaintenanceQueue(projectRoot), options);
177
+ const health = await maintenanceHealthSignals(projectRoot, options);
178
+ const reviewReasons = [...summary.reviewReasons];
179
+ if (health.memoryNearBudget) reviewReasons.push(`memory.md is near budget (${health.memoryBytes} bytes)`);
180
+ if (health.pageCountNearSearchCap) reviewReasons.push(`wiki page count ${health.pageCount} is near search cap ${health.searchCap}`);
181
+ if (health.lint && health.lint.issueCount > 0) {
182
+ reviewReasons.push(`lint has ${health.lint.errorCount} error(s) and ${health.lint.warningCount} warning(s)`);
183
+ }
184
+ return {
185
+ ...summary,
186
+ reviewDue: reviewReasons.length > 0,
187
+ reviewReasons,
188
+ health,
189
+ recommendedCommands: recommendedMaintenanceCommands(projectRoot),
190
+ };
191
+ }
192
+
193
+ function recommendedMaintenanceCommands(projectRoot) {
194
+ return [
195
+ `llm-wiki lint --workspace ${projectRoot}`,
196
+ `llm-wiki maintenance --workspace ${projectRoot}`,
197
+ `llm-wiki consolidate --workspace ${projectRoot} --dry-run`,
198
+ `llm-wiki consolidate --workspace ${projectRoot}`,
199
+ ];
200
+ }
201
+
202
+ async function maintenanceHealthSignals(projectRoot, options = {}) {
203
+ const memoryText = await readText(join(projectRoot, 'llm-wiki', 'wiki', 'memory.md'), '');
204
+ const memoryBytes = Buffer.byteLength(memoryText, 'utf8');
205
+ let pageCount = 0;
206
+ const searchCap = options.searchCap || DEFAULT_MAX_WIKI_FILES;
207
+ try {
208
+ pageCount = (await collectWikiPages(projectRoot, { maxFiles: searchCap })).length;
209
+ } catch {
210
+ pageCount = 0;
211
+ }
212
+ let lint = null;
213
+ if (options.includeLint && !options.skipLint) {
214
+ try {
215
+ const module = await import('./wiki-lint.js');
216
+ const result = await module.runLint(projectRoot, { maxFiles: options.maxFiles || 1000, skipMaintenance: true });
217
+ lint = {
218
+ ok: result.ok,
219
+ issueCount: result.issueCount,
220
+ errorCount: result.errorCount,
221
+ warningCount: result.warningCount,
222
+ };
223
+ } catch {
224
+ lint = null;
225
+ }
226
+ }
227
+ return {
228
+ memoryBytes,
229
+ memoryNearBudget: memoryBytes >= MEMORY_NEAR_BUDGET_BYTES,
230
+ memoryOversized: memoryBytes > MEMORY_BYTE_LIMIT,
231
+ pageCount,
232
+ searchCap,
233
+ pageCountNearSearchCap: pageCount >= Math.floor(searchCap * 0.8),
234
+ lint,
235
+ };
153
236
  }
154
237
 
155
238
  export async function appendMaintenanceItem(projectRoot, item) {
@@ -225,11 +308,11 @@ export async function recoverStaleTurnStates(projectRoot, options = {}) {
225
308
  }
226
309
 
227
310
  export function formatMaintenanceContext(summary, options = {}) {
311
+ if (!summary.reviewDue) return '';
228
312
  const eventName = options.eventName || '';
229
313
  const defaultLimit = eventName === 'SessionStart' || eventName === 'InstructionsLoaded' ? 1 : 5;
230
314
  const limit = options.limit || defaultLimit;
231
315
  let pending = summary.pending.slice(0, limit);
232
- if (pending.length === 0) return '';
233
316
 
234
317
  if (eventName === 'UserPromptSubmit') {
235
318
  if (!isMaintenanceRelatedQuery(options.query || '', summary.pending)) return '';
@@ -242,6 +325,7 @@ export function formatMaintenanceContext(summary, options = {}) {
242
325
  eventName === 'UserPromptSubmit'
243
326
  ? 'LLM Wiki maintenance status:'
244
327
  : 'LLM Wiki maintenance status:',
328
+ `- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
245
329
  `- pending review items: ${summary.pendingCount}. 현재 요청이 우선이며, 관련 있을 때만 durable wiki 정리에 사용한다.`,
246
330
  ];
247
331
  for (const item of pending) {
@@ -258,14 +342,25 @@ export function formatMaintenanceResult(summary) {
258
342
  'llm-wiki maintenance',
259
343
  `- queue: ${summary.path}`,
260
344
  `- pending: ${summary.pendingCount}`,
345
+ `- stale pending: ${summary.stalePendingCount || 0}`,
261
346
  `- done: ${summary.doneCount}`,
262
347
  `- skipped: ${summary.skippedCount}`,
348
+ `- review due: ${summary.reviewDue ? 'yes' : 'no'}`,
263
349
  ];
350
+ if ((summary.reviewReasons || []).length > 0) {
351
+ lines.push(`- review reasons: ${summary.reviewReasons.join('; ')}`);
352
+ }
264
353
  if (summary.pending.length > 0) {
265
354
  lines.push('', 'Pending:');
266
355
  for (const item of summary.pending.slice(0, 10)) {
267
356
  lines.push(`- ${item.topic || item.id}: ${item.source} -> ${item.suggested_target}`);
268
357
  }
269
358
  }
359
+ if ((summary.recommendedCommands || []).length > 0) {
360
+ lines.push('', 'Recommended commands:');
361
+ for (const command of summary.recommendedCommands) {
362
+ lines.push(`- ${command}`);
363
+ }
364
+ }
270
365
  return lines.join('\n');
271
366
  }
package/src/state.js CHANGED
@@ -38,6 +38,22 @@ export async function clearTurnState(projectRoot, payload) {
38
38
  await unlink(statePath(projectRoot, payload)).catch(() => {});
39
39
  }
40
40
 
41
+ export function compactRecoveryPath(projectRoot, payload) {
42
+ return join(kitDataDir(), 'compact-recovery', `${sessionKey(projectRoot, payload)}.json`);
43
+ }
44
+
45
+ export async function readCompactRecovery(projectRoot, payload) {
46
+ return readJson(compactRecoveryPath(projectRoot, payload), null);
47
+ }
48
+
49
+ export async function writeCompactRecovery(projectRoot, payload, recovery) {
50
+ await writeJson(compactRecoveryPath(projectRoot, payload), recovery);
51
+ }
52
+
53
+ export async function clearCompactRecovery(projectRoot, payload) {
54
+ await unlink(compactRecoveryPath(projectRoot, payload)).catch(() => {});
55
+ }
56
+
41
57
  export async function rememberQuestion(projectRoot, payload, prompt) {
42
58
  const state = await readTurnState(projectRoot, payload);
43
59
  const clean = summarizeForStorage(prompt, 3000);
package/src/templates.js CHANGED
@@ -15,7 +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 raw envelope와 필요한 live Q&A를 안전하게 남긴다. \`wiki/queries\`/\`wiki/decisions\` 자동 승격은 기본값이 아니며, durable 지식은 중요도와 동의 흐름에 따라 agent가 기존 정식 wiki 문서에 합친다.
18
- - hook은 종료/시작 경계에서 정말 필요한 정리 후보를 \`llm-wiki/outputs/maintenance/queue.md\`에 남길 수 있다. pending 항목은 현재 응답을 지연시키지 않는 범위에서 agent가 병합하거나 done/skipped로 표시한다.
18
+ - hook은 종료/시작 경계에서 정말 필요한 정리 후보를 \`llm-wiki/outputs/maintenance/queue.md\`에 남길 수 있다. 정기 maintenance는 agent-side soft reminder이며, pending 항목은 현재 응답을 지연시키지 않는 범위에서 agent가 병합하거나 done/skipped로 표시한다.
19
19
  - 새 문서를 만들기 전에 기존 wiki 문서를 먼저 찾아 갱신한다. 반복해서 쓸 사실은 \`outputs/questions/\`에만 두지 말고 적절한 wiki 문서에 합친다.
20
20
  - 일회성 작업 기록은 필요할 때 \`llm-wiki/outputs/questions/\`에 보존하고, 재사용 가능한 사실/지식은 승인된 경우 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 반영한다.
21
21
  - 검증 명령, 근거 파일, 불확실한 점을 함께 남긴다. 추론은 추론이라고 표시하고, 모순은 지우지 말고 Open Questions 또는 Contradictions에 남긴다.
@@ -49,7 +49,8 @@ Codex와 Claude Code를 평소처럼 사용하는 동안 living Markdown LLM Wik
49
49
  - 오래 남길 내용이 생기면 새 문서부터 만들지 말고 기존 \`wiki/\` 문서를 먼저 찾아 갱신한다.
50
50
  - 단순 답변, 상태 확인, 일회성 대화는 과도하게 \`wiki/queries\`나 maintenance로 승격하지 않는다.
51
51
  - 반복해서 쓸 지식은 중요도와 동의 흐름에 따라 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
52
- - hook이 만든 \`outputs/maintenance/queue.md\` pending 항목은 현재 요청과 관련 있을 때 확인하고, 기존 정식 wiki 문서에 병합한 뒤 done 또는 skipped로 표시한다.
52
+ - hook이 만든 \`outputs/maintenance/queue.md\` pending 항목은 현재 요청과 관련 있거나 review due 안내가 있을 때 확인하고, 기존 정식 wiki 문서에 병합한 뒤 done 또는 skipped로 표시한다.
53
+ - 정기 maintenance는 자동 수정이 아니라 agent review다. \`SessionStart\`/\`InstructionsLoaded\`에서만 짧게 안내되고, \`UserPromptSubmit\`에서는 사용자가 wiki/maintenance/정리 관련 질문을 한 경우에만 안내된다.
53
54
  - \`wiki/memory.md\`는 짧게 유지한다. 긴 설명 대신 현재 상태와 중요한 문서 링크를 둔다.
54
55
  - 모순은 덮어쓰지 말고 \`Contradictions\` 또는 \`Open Questions\`에 보존한다.
55
56
  - 인증값, token, password, private key, \`.env\` 원문은 wiki에 저장하지 않는다.
@@ -182,7 +183,7 @@ export function procedure(name) {
182
183
  3. 관련 \`wiki/\` 문서를 최소한으로 읽는다.
183
184
  4. 정확한 근거가 필요할 때만 raw source를 확인한다.
184
185
  5. 검증된 사실과 추론을 분리한다.
185
- 6. 수동 확인이 필요할 때만 \`llm-wiki context "<query>"\`를 쓴다. 과거 episodic query까지 봐야 할 때는 \`--include-episodic\`을 붙인다.
186
+ 6. 수동 확인이 필요할 때만 \`llm-wiki context "<query>"\`를 쓴다. 과거 episodic query/context까지 봐야 할 때는 \`--include-episodic\`을 붙이고, archived/superseded page까지 봐야 할 때만 \`--include-archived\`를 붙인다.
186
187
  7. 일회성 답변은 durable wiki로 승격하지 않는다. 필요한 작업 turn만 \`outputs/questions/\`에 남긴다.
187
188
  8. 반복해서 쓸 사실/지식은 중요도와 동의 흐름에 따라 \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, \`procedures/\`에 합친다.
188
189
  `,
@@ -190,7 +191,15 @@ export function procedure(name) {
190
191
 
191
192
  \`llm-wiki lint --workspace <project>\`는 agent가 필요할 때 쓰는 wiki 건강 점검 도구다. 사용자가 매번 실행해야 하는 명령이 아니다.
192
193
 
193
- 점검 대상: 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.
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.
195
+
196
+ 정기 maintenance는 사용자가 매 turn 실행할 필요가 없는 agent-side task다. 필요할 때 agent가 다음 순서로 확인한다:
197
+
198
+ 1. \`llm-wiki lint --workspace <project>\`
199
+ 2. \`llm-wiki maintenance --workspace <project>\`
200
+ 3. pending item을 기존 durable page에 병합한 뒤 \`done\` 또는 \`skipped\`로 표시한다.
201
+ 4. \`llm-wiki consolidate --workspace <project> --dry-run\`
202
+ 5. 필요할 때 \`llm-wiki consolidate --workspace <project>\`
194
203
 
195
204
  자동 수정은 확실히 kit가 관리하는 영역에만 적용한다. 사용자 편집 가능성이 있는 문서는 덮어쓰지 말고 다음 작업 context에 정리 필요성을 올린다.
196
205
  `,
package/src/wiki-lint.js CHANGED
@@ -8,10 +8,19 @@ import {
8
8
  buildAliasMap,
9
9
  buildWikiGraph,
10
10
  collectWikiPages,
11
+ DEFAULT_MAX_WIKI_FILES,
12
+ MEMORY_BYTE_LIMIT,
11
13
  normalizeTarget,
12
14
  resolveWikiLink,
13
15
  wikiRoot,
14
16
  } from './wiki-model.js';
17
+ import {
18
+ hasSupersession,
19
+ isArchivedPage,
20
+ isEpisodicLayerPage,
21
+ isPromotedDurablePage,
22
+ isStalePage,
23
+ } from './wiki-visibility.js';
15
24
 
16
25
  const VALID_TYPES = new Set([
17
26
  'source',
@@ -29,6 +38,9 @@ const VALID_STATUS = new Set(['draft', 'reviewed', 'stale', 'archived']);
29
38
  const VALID_CONFIDENCE = new Set(['high', 'medium', 'low']);
30
39
  const VALID_MEMORY_TYPES = new Set(['semantic', 'episodic', 'procedural']);
31
40
  const CORE_PAGES = new Set(['wiki/index.md', 'wiki/log.md', 'wiki/memory.md']);
41
+ const MEMORY_NEAR_BUDGET_BYTES = 20 * 1024;
42
+ const HIDDEN_PAGE_WARNING_THRESHOLD = 50;
43
+ const SEARCH_CAP_WARNING_RATIO = 0.8;
32
44
 
33
45
  function issue(severity, code, path, message) {
34
46
  return { severity, code, path, message };
@@ -125,8 +137,9 @@ export async function runLint(projectRoot, options = {}) {
125
137
  return lintResult(projectRoot, [], issues);
126
138
  }
127
139
 
140
+ const maxFiles = options.maxFiles || 1000;
128
141
  const pages = await collectWikiPages(projectRoot, {
129
- maxFiles: options.maxFiles || 1000,
142
+ maxFiles,
130
143
  maxChars: options.maxChars || 75000,
131
144
  });
132
145
  const byRel = new Map(pages.map((page) => [page.rel, page]));
@@ -226,9 +239,23 @@ export async function runLint(projectRoot, options = {}) {
226
239
  }
227
240
 
228
241
  const memoryText = await readText(join(projectRoot, 'llm-wiki', 'wiki', 'memory.md'), '');
229
- if (Buffer.byteLength(memoryText, 'utf8') > 25000) {
230
- issues.push(issue('warning', 'memory-too-large', 'wiki/memory.md', 'memory.md is larger than the hook excerpt budget'));
242
+ const memoryBytes = Buffer.byteLength(memoryText, 'utf8');
243
+ if (memoryBytes > MEMORY_BYTE_LIMIT) {
244
+ issues.push(issue('warning', 'memory-too-large', 'wiki/memory.md', `memory.md is oversized (${memoryBytes} bytes) and larger than the hook excerpt budget`));
245
+ } else if (memoryBytes >= MEMORY_NEAR_BUDGET_BYTES) {
246
+ issues.push(issue('warning', 'memory-near-budget', 'wiki/memory.md', `memory.md is near the hook excerpt budget (${memoryBytes} bytes)`));
247
+ }
248
+
249
+ const searchCap = options.maxFiles || DEFAULT_MAX_WIKI_FILES;
250
+ if (pages.length >= Math.floor(searchCap * SEARCH_CAP_WARNING_RATIO)) {
251
+ issues.push(issue('warning', 'wiki-page-count-near-search-cap', 'llm-wiki/wiki', `wiki page count ${pages.length} is near the default search cap ${searchCap}`));
231
252
  }
253
+
254
+ const hiddenEpisodicPages = pages.filter((page) => isEpisodicLayerPage(page) && !isPromotedDurablePage(page));
255
+ if (hiddenEpisodicPages.length >= HIDDEN_PAGE_WARNING_THRESHOLD) {
256
+ issues.push(issue('warning', 'hidden-episodic-growth', 'llm-wiki/wiki', `${hiddenEpisodicPages.length} default-hidden episodic/context/session pages may add wiki growth and search noise`));
257
+ }
258
+
232
259
  for (const page of pages.filter(isOrphanCandidate)) {
233
260
  const backlinks = graph.backlinks.get(page.rel);
234
261
  if (!backlinks || backlinks.size === 0) {
@@ -236,6 +263,15 @@ export async function runLint(projectRoot, options = {}) {
236
263
  }
237
264
  }
238
265
 
266
+ for (const page of pages) {
267
+ if (!isArchivedPage(page) && !isStalePage(page)) continue;
268
+ const backlinks = graph.backlinks.get(page.rel);
269
+ const outlinks = graph.outlinks.get(page.rel);
270
+ if (!hasSupersession(page) && (!backlinks || backlinks.size === 0) && (!outlinks || outlinks.size === 0)) {
271
+ issues.push(issue('warning', 'stale-archived-discoverability', page.rel, 'stale/archived page has no supersession metadata or wiki links for discoverability'));
272
+ }
273
+ }
274
+
239
275
  const projectState = await inspectProjectState(projectRoot).catch(() => null);
240
276
  for (const file of projectState?.managedFiles || []) {
241
277
  if (file.needsAttention) {
@@ -245,12 +281,14 @@ export async function runLint(projectRoot, options = {}) {
245
281
  }
246
282
  }
247
283
 
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`));
284
+ if (!options.skipMaintenance) {
285
+ const maintenance = await maintenanceSummary(projectRoot, { skipLint: true }).catch(() => null);
286
+ if (maintenance?.tooManyPending) {
287
+ 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`));
288
+ }
289
+ if ((maintenance?.stalePending || []).length > 0) {
290
+ issues.push(issue('warning', 'maintenance-stale-pending', 'outputs/maintenance/queue.md', `${maintenance.stalePending.length} pending maintenance item(s) are older than ${maintenance.stalePendingDays} days`));
291
+ }
254
292
  }
255
293
 
256
294
  return lintResult(projectRoot, pages, issues);