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.
- package/README.md +7 -6
- package/docs/concepts.md +4 -3
- package/docs/integrations/claude-code.md +2 -2
- package/docs/integrations/codex.md +3 -3
- package/docs/manual.md +23 -7
- package/docs/operations.md +23 -4
- package/package.json +1 -1
- package/src/cli.js +4 -2
- package/src/compact-capture.js +107 -29
- package/src/consolidate.js +62 -29
- package/src/hook.js +25 -9
- package/src/maintenance.js +97 -2
- package/src/state.js +16 -0
- package/src/templates.js +13 -4
- package/src/wiki-lint.js +47 -9
- package/src/wiki-search.js +84 -19
- package/src/wiki-visibility.js +76 -0
package/src/consolidate.js
CHANGED
|
@@ -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
|
|
49
|
+
return isPromotedDurablePage(page);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
) {
|
|
61
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
`-
|
|
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
|
-
|
|
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
|
-
`-
|
|
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(
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
|
133
|
-
|
|
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
|
-
|
|
164
|
-
return
|
|
179
|
+
await finalizeCompactRecovery(projectRoot, payload).catch(() => {});
|
|
180
|
+
return {};
|
|
165
181
|
}
|
|
166
182
|
return handlePreCompactCapture(projectRoot, provider, eventName, payload);
|
|
167
183
|
}
|
package/src/maintenance.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|