llm-wiki-kit 0.2.16 → 0.2.18
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 +4 -2
- package/docs/concepts.md +6 -5
- package/docs/integrations/claude-code.md +3 -3
- package/docs/integrations/codex.md +3 -3
- package/docs/manual.md +8 -2
- package/docs/operations.md +9 -3
- package/docs/troubleshooting.md +2 -1
- package/package.json +1 -1
- package/src/capture-policy.js +9 -0
- package/src/cli.js +15 -3
- package/src/compact-capture.js +1 -0
- package/src/consolidate.js +19 -13
- package/src/evidence.js +31 -14
- package/src/hook.js +31 -9
- package/src/language.js +15 -0
- package/src/live-qa.js +34 -11
- package/src/maintenance.js +229 -1
- package/src/project.js +48 -2
- package/src/state.js +43 -1
- package/src/templates.js +11 -9
- package/src/wiki-search.js +8 -8
package/src/language.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homeDir, readJson, readText } from './fs-utils.js';
|
|
|
3
3
|
|
|
4
4
|
const HANGUL_RE = /[\u3131-\u318e\uac00-\ud7a3]/g;
|
|
5
5
|
const LATIN_RE = /[A-Za-z]/g;
|
|
6
|
+
const SYNTHETIC_PLAN_PROMPT_RE = /^\s*implement\s+the\s+plan\.?\s*$/i;
|
|
6
7
|
|
|
7
8
|
export function normalizeLanguage(value) {
|
|
8
9
|
const text = String(value || '').trim().toLowerCase();
|
|
@@ -14,9 +15,14 @@ export function normalizeLanguage(value) {
|
|
|
14
15
|
return null;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export function isSyntheticPlanPrompt(value) {
|
|
19
|
+
return SYNTHETIC_PLAN_PROMPT_RE.test(String(value || ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
export function detectTextLanguage(value) {
|
|
18
23
|
const text = String(value || '').normalize('NFC');
|
|
19
24
|
if (!text.trim()) return null;
|
|
25
|
+
if (isSyntheticPlanPrompt(text)) return null;
|
|
20
26
|
const hangul = (text.match(HANGUL_RE) || []).length;
|
|
21
27
|
const latin = (text.match(LATIN_RE) || []).length;
|
|
22
28
|
if (hangul >= 2) return 'ko';
|
|
@@ -25,6 +31,11 @@ export function detectTextLanguage(value) {
|
|
|
25
31
|
return null;
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
async function projectPreferredLanguage(projectRoot) {
|
|
35
|
+
const state = await readJson(join(projectRoot, 'llm-wiki', '.kit-state.json'), null);
|
|
36
|
+
return normalizeLanguage(state?.preferredLanguage || state?.preferred_language);
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
async function claudeSettingsLanguage(projectRoot) {
|
|
29
40
|
const paths = [
|
|
30
41
|
join(homeDir(), '.claude', 'settings.json'),
|
|
@@ -61,6 +72,10 @@ async function instructionFileLanguage(projectRoot) {
|
|
|
61
72
|
export async function resolveLanguage(projectRoot, options = {}) {
|
|
62
73
|
const promptLanguage = detectTextLanguage(options.prompt || '');
|
|
63
74
|
if (promptLanguage) return promptLanguage;
|
|
75
|
+
const rememberedLanguage = normalizeLanguage(options.rememberedLanguage);
|
|
76
|
+
if (rememberedLanguage) return rememberedLanguage;
|
|
77
|
+
const preferredLanguage = await projectPreferredLanguage(projectRoot);
|
|
78
|
+
if (preferredLanguage) return preferredLanguage;
|
|
64
79
|
if (String(options.provider || '').toLowerCase() === 'claude') {
|
|
65
80
|
const settingsLanguage = await claudeSettingsLanguage(projectRoot);
|
|
66
81
|
if (settingsLanguage) return settingsLanguage;
|
package/src/live-qa.js
CHANGED
|
@@ -95,31 +95,52 @@ async function targetChunkPath(projectRoot, day, block, options = {}) {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export function formatLiveQaBlock(entry) {
|
|
98
|
+
const korean = entry.language === 'ko';
|
|
99
|
+
const labels = korean
|
|
100
|
+
? {
|
|
101
|
+
question: '### 질문',
|
|
102
|
+
work: '### 작업',
|
|
103
|
+
result: '### 결과',
|
|
104
|
+
changedFiles: '### 변경 파일',
|
|
105
|
+
verification: '### 검증',
|
|
106
|
+
followUp: '### 후속 조치',
|
|
107
|
+
fallback: '작업/결정 중심 live Q&A 기록입니다. 재사용할 사실은 사용자가 요청했거나 명확히 중요할 때만 기존 durable wiki 문서에 병합합니다.',
|
|
108
|
+
}
|
|
109
|
+
: {
|
|
110
|
+
question: '### Question',
|
|
111
|
+
work: '### Work',
|
|
112
|
+
result: '### Result',
|
|
113
|
+
changedFiles: '### Changed files',
|
|
114
|
+
verification: '### Verification',
|
|
115
|
+
followUp: '### Follow-up',
|
|
116
|
+
fallback: 'Work/decision-focused live Q&A record. Merge reusable facts into existing durable wiki pages only when the user asks or the fact is clearly important.',
|
|
117
|
+
};
|
|
98
118
|
return [
|
|
99
119
|
`\n## ${timeKst()} KST - ${entry.topic || 'session turn'}`,
|
|
100
120
|
'',
|
|
101
|
-
|
|
121
|
+
labels.question,
|
|
102
122
|
entry.question || '(not captured)',
|
|
103
123
|
'',
|
|
104
|
-
|
|
124
|
+
labels.work,
|
|
105
125
|
entry.work || '(not captured)',
|
|
106
126
|
'',
|
|
107
|
-
|
|
127
|
+
labels.result,
|
|
108
128
|
entry.result || '(not captured)',
|
|
109
129
|
'',
|
|
110
|
-
|
|
130
|
+
labels.changedFiles,
|
|
111
131
|
entry.changedFiles || '(not captured)',
|
|
112
132
|
'',
|
|
113
|
-
|
|
133
|
+
labels.verification,
|
|
114
134
|
entry.verification || '(not captured)',
|
|
115
135
|
'',
|
|
116
|
-
|
|
117
|
-
entry.followUp ||
|
|
136
|
+
labels.followUp,
|
|
137
|
+
entry.followUp || labels.fallback,
|
|
118
138
|
'',
|
|
119
139
|
].join('\n');
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
export async function refreshLiveQaIndex(projectRoot, day = todayKst()) {
|
|
142
|
+
export async function refreshLiveQaIndex(projectRoot, day = todayKst(), options = {}) {
|
|
143
|
+
const korean = options.language === 'ko';
|
|
123
144
|
const dayDir = liveQaDayDir(projectRoot, day);
|
|
124
145
|
const files = await listChunkFiles(dayDir);
|
|
125
146
|
const rows = [];
|
|
@@ -134,13 +155,15 @@ export async function refreshLiveQaIndex(projectRoot, day = todayKst()) {
|
|
|
134
155
|
const content = [
|
|
135
156
|
`# Live Q&A ${day}`,
|
|
136
157
|
'',
|
|
137
|
-
|
|
158
|
+
korean
|
|
159
|
+
? '의미 있는 작업과 결정 turn을 날짜별 chunk로 보관하는 live Q&A archive입니다.'
|
|
160
|
+
: 'Chunked live Q&A archive for meaningful work and decision turns.',
|
|
138
161
|
'',
|
|
139
162
|
`- chunks: ${files.length}`,
|
|
140
163
|
`- turns: ${totalBlocks}`,
|
|
141
164
|
`- updated_at: ${new Date().toISOString()}`,
|
|
142
165
|
'',
|
|
143
|
-
'## Chunks',
|
|
166
|
+
korean ? '## 청크' : '## Chunks',
|
|
144
167
|
'',
|
|
145
168
|
rows.length > 0 ? rows.join('\n') : '(none)',
|
|
146
169
|
'',
|
|
@@ -153,7 +176,7 @@ export async function appendLiveQa(projectRoot, entry, options = {}) {
|
|
|
153
176
|
const block = redactText(formatLiveQaBlock(entry), 12000);
|
|
154
177
|
const path = await targetChunkPath(projectRoot, day, block, options);
|
|
155
178
|
await appendText(path, block);
|
|
156
|
-
await refreshLiveQaIndex(projectRoot, day);
|
|
179
|
+
await refreshLiveQaIndex(projectRoot, day, { language: entry.language });
|
|
157
180
|
return path;
|
|
158
181
|
}
|
|
159
182
|
|
package/src/maintenance.js
CHANGED
|
@@ -13,7 +13,11 @@ const DEFAULT_STALE_PENDING_DAYS = 7;
|
|
|
13
13
|
const DEFAULT_PENDING_LIMIT = 20;
|
|
14
14
|
const DEFAULT_REVIEW_PENDING_LIMIT = 5;
|
|
15
15
|
const DEFAULT_REVIEW_INTERVAL_DAYS = 14;
|
|
16
|
+
const DEFAULT_LIFECYCLE_PENDING_THRESHOLD = 15;
|
|
17
|
+
const DEFAULT_LOW_SIGNAL_SKIP_DAYS = 14;
|
|
18
|
+
const DEFAULT_REVIEWED_KEEP = 10;
|
|
16
19
|
const MEMORY_NEAR_BUDGET_BYTES = 20 * 1024;
|
|
20
|
+
const SIGNAL_LEVELS = new Set(['explicit', 'high', 'low', 'recovered']);
|
|
17
21
|
|
|
18
22
|
function queuePath(projectRoot) {
|
|
19
23
|
return join(projectRoot, MAINTENANCE_QUEUE_REL);
|
|
@@ -26,6 +30,7 @@ function queueHeader() {
|
|
|
26
30
|
'Candidates to merge into durable wiki pages. Hooks only create candidates; the active agent reviews and merges them into existing durable wiki documents.',
|
|
27
31
|
'',
|
|
28
32
|
'Status values: pending, approved, done, skipped.',
|
|
33
|
+
'Lifecycle keeps explicit/high-signal candidates visible, may skip old low-signal candidates, and archives old reviewed items.',
|
|
29
34
|
'',
|
|
30
35
|
].join('\n');
|
|
31
36
|
}
|
|
@@ -55,6 +60,44 @@ function sanitizeField(value, maxLength = 500) {
|
|
|
55
60
|
return summarizeForStorage(String(value || '').replace(/\s+/g, ' ').trim(), maxLength);
|
|
56
61
|
}
|
|
57
62
|
|
|
63
|
+
function normalizeSignalLevel(value) {
|
|
64
|
+
const text = String(value || '').trim().toLowerCase();
|
|
65
|
+
return SIGNAL_LEVELS.has(text) ? text : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inferSignalLevelFromReason(reason, resultMissing = false) {
|
|
69
|
+
const text = String(reason || '').toLowerCase();
|
|
70
|
+
if (resultMissing || text.includes('recovered from stale turn state')) return 'recovered';
|
|
71
|
+
if (text.includes('explicit durable documentation request')) return 'explicit';
|
|
72
|
+
if (text.includes('precompact durable candidate')) return 'high';
|
|
73
|
+
if (text.includes('suggested durable wiki candidate')) return 'high';
|
|
74
|
+
if (text.includes('durable wiki review')) return 'high';
|
|
75
|
+
return 'high';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function signalLevelForItem(item) {
|
|
79
|
+
if (item?.result_missing) return 'recovered';
|
|
80
|
+
return normalizeSignalLevel(item?.signal_level || item?.fields?.signal_level) ||
|
|
81
|
+
inferSignalLevelFromReason(item?.reason || item?.fields?.reason, item?.result_missing);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isStaleByDays(item, days, nowMs = Date.now()) {
|
|
85
|
+
const time = Date.parse(item?.created_at || item?.last_seen_at || '');
|
|
86
|
+
return Number.isFinite(time) && nowMs - time >= days * 24 * 60 * 60 * 1000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function reviewedTimeMs(item) {
|
|
90
|
+
const values = [item?.reviewed_at, item?.last_seen_at, item?.created_at]
|
|
91
|
+
.map((value) => Date.parse(value || ''))
|
|
92
|
+
.filter(Number.isFinite);
|
|
93
|
+
return values.length > 0 ? Math.max(...values) : 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function archiveMonth(item) {
|
|
97
|
+
const time = reviewedTimeMs(item) || Date.now();
|
|
98
|
+
return new Date(time).toISOString().slice(0, 7);
|
|
99
|
+
}
|
|
100
|
+
|
|
58
101
|
function inferSuggestedTarget(entry, source) {
|
|
59
102
|
const text = `${entry?.question || ''}\n${entry?.result || ''}\n${entry?.work || ''}\n${source || ''}`.toLowerCase();
|
|
60
103
|
if (String(source || '').includes('wiki/decisions/') || /decision|decided|결정|선택|채택|확정/.test(text)) return 'wiki/decisions/';
|
|
@@ -78,8 +121,10 @@ function itemBlock(item) {
|
|
|
78
121
|
last_seen_at: item.last_seen_at,
|
|
79
122
|
source: item.source,
|
|
80
123
|
suggested_target: item.suggested_target,
|
|
124
|
+
signal_level: item.signal_level,
|
|
81
125
|
target: item.target,
|
|
82
126
|
reviewed_at: item.reviewed_at,
|
|
127
|
+
archived_at: item.archived_at,
|
|
83
128
|
review_note: item.review_note,
|
|
84
129
|
evidence_refs: normalizeEvidenceRefs(item.evidence_refs).length > 0
|
|
85
130
|
? JSON.stringify(normalizeEvidenceRefs(item.evidence_refs))
|
|
@@ -93,8 +138,10 @@ function itemBlock(item) {
|
|
|
93
138
|
'last_seen_at',
|
|
94
139
|
'source',
|
|
95
140
|
'suggested_target',
|
|
141
|
+
'signal_level',
|
|
96
142
|
'target',
|
|
97
143
|
'reviewed_at',
|
|
144
|
+
'archived_at',
|
|
98
145
|
'review_note',
|
|
99
146
|
'evidence_refs',
|
|
100
147
|
'reason',
|
|
@@ -159,6 +206,7 @@ export async function readMaintenanceQueue(projectRoot) {
|
|
|
159
206
|
evidence_refs: parseEvidenceRefsField(item.fields.evidence_refs),
|
|
160
207
|
evidenceRefs: parseEvidenceRefsField(item.fields.evidence_refs),
|
|
161
208
|
result_missing: String(item.fields.result_missing || '').toLowerCase() === 'true',
|
|
209
|
+
signal_level: normalizeSignalLevel(item.fields.signal_level),
|
|
162
210
|
})),
|
|
163
211
|
};
|
|
164
212
|
}
|
|
@@ -175,6 +223,168 @@ async function writeMaintenanceQueue(projectRoot, items) {
|
|
|
175
223
|
return path;
|
|
176
224
|
}
|
|
177
225
|
|
|
226
|
+
function lifecycleDefaults(options = {}) {
|
|
227
|
+
return {
|
|
228
|
+
pendingThreshold: options.pendingThreshold ?? DEFAULT_LIFECYCLE_PENDING_THRESHOLD,
|
|
229
|
+
lowSignalSkipDays: options.lowSignalSkipDays ?? DEFAULT_LOW_SIGNAL_SKIP_DAYS,
|
|
230
|
+
reviewedKeep: options.reviewedKeep ?? DEFAULT_REVIEWED_KEEP,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function lifecycleAction(action, item, extra = {}) {
|
|
235
|
+
return {
|
|
236
|
+
action,
|
|
237
|
+
id: item.id,
|
|
238
|
+
topic: item.topic,
|
|
239
|
+
status: item.status,
|
|
240
|
+
signal_level: signalLevelForItem(item),
|
|
241
|
+
source: item.source,
|
|
242
|
+
...extra,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function appendArchivedItems(projectRoot, items, now) {
|
|
247
|
+
const grouped = new Map();
|
|
248
|
+
for (const item of items) {
|
|
249
|
+
const month = archiveMonth(item);
|
|
250
|
+
if (!grouped.has(month)) grouped.set(month, []);
|
|
251
|
+
grouped.get(month).push(item);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const files = [];
|
|
255
|
+
for (const [month, monthItems] of grouped.entries()) {
|
|
256
|
+
const path = join(projectRoot, 'llm-wiki', 'outputs', 'maintenance', 'archive', `${month}.md`);
|
|
257
|
+
const existing = await readText(path, '');
|
|
258
|
+
const header = existing ? '' : [
|
|
259
|
+
'# LLM Wiki Maintenance Archive',
|
|
260
|
+
'',
|
|
261
|
+
`Reviewed queue items archived for ${month}.`,
|
|
262
|
+
'',
|
|
263
|
+
].join('\n');
|
|
264
|
+
const blocks = [];
|
|
265
|
+
for (const item of monthItems) {
|
|
266
|
+
if (existing.includes(`- id: ${item.id}`)) continue;
|
|
267
|
+
blocks.push(itemBlock({
|
|
268
|
+
...item,
|
|
269
|
+
fields: { ...(item.fields || {}) },
|
|
270
|
+
archived_at: now,
|
|
271
|
+
}).trimEnd());
|
|
272
|
+
}
|
|
273
|
+
if (blocks.length === 0) continue;
|
|
274
|
+
await appendText(path, `${header}${existing && !existing.endsWith('\n') ? '\n' : ''}${blocks.join('\n\n')}\n`);
|
|
275
|
+
files.push(path);
|
|
276
|
+
}
|
|
277
|
+
return files;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function applyMaintenanceLifecycle(projectRoot, options = {}) {
|
|
281
|
+
const queue = await readMaintenanceQueue(projectRoot);
|
|
282
|
+
const defaults = lifecycleDefaults(options);
|
|
283
|
+
const now = nowIso();
|
|
284
|
+
const actions = [];
|
|
285
|
+
const pending = queue.items.filter((item) => item.status === 'pending');
|
|
286
|
+
let nextItems = queue.items.map((item) => ({ ...item, fields: { ...(item.fields || {}) } }));
|
|
287
|
+
let approvedByLifecycle = 0;
|
|
288
|
+
let skippedByLifecycle = 0;
|
|
289
|
+
let archivedReviewedCount = 0;
|
|
290
|
+
const shouldApplyPendingLifecycle = pending.length >= defaults.pendingThreshold;
|
|
291
|
+
|
|
292
|
+
if (shouldApplyPendingLifecycle) {
|
|
293
|
+
nextItems = nextItems.map((item) => {
|
|
294
|
+
if (item.status !== 'pending') return item;
|
|
295
|
+
const signal = signalLevelForItem(item);
|
|
296
|
+
if (signal === 'explicit' || signal === 'high') {
|
|
297
|
+
approvedByLifecycle += 1;
|
|
298
|
+
actions.push(lifecycleAction('approve', item, { reason: `pending threshold ${defaults.pendingThreshold} reached` }));
|
|
299
|
+
const target = item.target || item.suggested_target || '';
|
|
300
|
+
const reviewNote = sanitizeField('Auto-approved by lifecycle because the pending queue reached the review threshold.', 500);
|
|
301
|
+
return {
|
|
302
|
+
...item,
|
|
303
|
+
status: 'approved',
|
|
304
|
+
signal_level: signal,
|
|
305
|
+
target,
|
|
306
|
+
reviewed_at: now,
|
|
307
|
+
last_seen_at: now,
|
|
308
|
+
review_note: item.review_note || reviewNote,
|
|
309
|
+
fields: {
|
|
310
|
+
...(item.fields || {}),
|
|
311
|
+
signal_level: signal,
|
|
312
|
+
...(target ? { target } : {}),
|
|
313
|
+
reviewed_at: now,
|
|
314
|
+
last_seen_at: now,
|
|
315
|
+
review_note: item.review_note || reviewNote,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (signal === 'low' && isStaleByDays(item, defaults.lowSignalSkipDays)) {
|
|
320
|
+
skippedByLifecycle += 1;
|
|
321
|
+
actions.push(lifecycleAction('skip', item, { reason: `low-signal item older than ${defaults.lowSignalSkipDays} days` }));
|
|
322
|
+
const reviewNote = sanitizeField(`Auto-skipped by lifecycle because this low-signal candidate was older than ${defaults.lowSignalSkipDays} days.`, 500);
|
|
323
|
+
return {
|
|
324
|
+
...item,
|
|
325
|
+
status: 'skipped',
|
|
326
|
+
signal_level: signal,
|
|
327
|
+
reviewed_at: now,
|
|
328
|
+
last_seen_at: now,
|
|
329
|
+
review_note: item.review_note || reviewNote,
|
|
330
|
+
fields: {
|
|
331
|
+
...(item.fields || {}),
|
|
332
|
+
signal_level: signal,
|
|
333
|
+
reviewed_at: now,
|
|
334
|
+
last_seen_at: now,
|
|
335
|
+
review_note: item.review_note || reviewNote,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
...item,
|
|
341
|
+
signal_level: signal,
|
|
342
|
+
fields: {
|
|
343
|
+
...(item.fields || {}),
|
|
344
|
+
signal_level: signal,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const reviewed = nextItems
|
|
351
|
+
.filter((item) => item.status === 'done' || item.status === 'skipped')
|
|
352
|
+
.sort((a, b) => reviewedTimeMs(b) - reviewedTimeMs(a));
|
|
353
|
+
const reviewedToArchive = reviewed.slice(defaults.reviewedKeep);
|
|
354
|
+
if (reviewedToArchive.length > 0) {
|
|
355
|
+
archivedReviewedCount = reviewedToArchive.length;
|
|
356
|
+
for (const item of reviewedToArchive) {
|
|
357
|
+
actions.push(lifecycleAction('archive-reviewed', item, { reason: `keeping latest ${defaults.reviewedKeep} reviewed items in queue.md` }));
|
|
358
|
+
}
|
|
359
|
+
const archiveIds = new Set(reviewedToArchive.map((item) => item.id));
|
|
360
|
+
nextItems = nextItems.filter((item) => !archiveIds.has(item.id));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (!options.dryRun && actions.length > 0) {
|
|
364
|
+
if (reviewedToArchive.length > 0) {
|
|
365
|
+
await appendArchivedItems(projectRoot, reviewedToArchive, now);
|
|
366
|
+
}
|
|
367
|
+
if (actions.length > 0 || queue.exists) {
|
|
368
|
+
await writeMaintenanceQueue(projectRoot, nextItems);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
workspace: projectRoot,
|
|
374
|
+
path: queue.path,
|
|
375
|
+
dryRun: Boolean(options.dryRun),
|
|
376
|
+
appliedPendingLifecycle: shouldApplyPendingLifecycle,
|
|
377
|
+
pendingThreshold: defaults.pendingThreshold,
|
|
378
|
+
lowSignalSkipDays: defaults.lowSignalSkipDays,
|
|
379
|
+
reviewedKeep: defaults.reviewedKeep,
|
|
380
|
+
lifecycleActions: actions,
|
|
381
|
+
actionCount: actions.length,
|
|
382
|
+
approvedByLifecycle,
|
|
383
|
+
skippedByLifecycle,
|
|
384
|
+
archivedReviewedCount,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
178
388
|
export function summarizeMaintenanceQueue(queue, options = {}) {
|
|
179
389
|
const staleDays = options.staleDays ?? DEFAULT_STALE_PENDING_DAYS;
|
|
180
390
|
const pendingLimit = options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
|
|
@@ -240,6 +450,10 @@ export async function maintenanceSummary(projectRoot, options = {}) {
|
|
|
240
450
|
reviewReasons,
|
|
241
451
|
health,
|
|
242
452
|
recommendedCommands: recommendedMaintenanceCommands(projectRoot),
|
|
453
|
+
lifecycleActions: options.lifecycleActions || [],
|
|
454
|
+
approvedByLifecycle: options.approvedByLifecycle || 0,
|
|
455
|
+
skippedByLifecycle: options.skippedByLifecycle || 0,
|
|
456
|
+
archivedReviewedCount: options.archivedReviewedCount || 0,
|
|
243
457
|
};
|
|
244
458
|
}
|
|
245
459
|
|
|
@@ -247,6 +461,7 @@ function recommendedMaintenanceCommands(projectRoot) {
|
|
|
247
461
|
return [
|
|
248
462
|
`llm-wiki lint --workspace ${projectRoot}`,
|
|
249
463
|
`llm-wiki maintenance --workspace ${projectRoot}`,
|
|
464
|
+
`llm-wiki maintenance --workspace ${projectRoot} --apply-lifecycle --dry-run`,
|
|
250
465
|
`llm-wiki consolidate --workspace ${projectRoot} --dry-run`,
|
|
251
466
|
`llm-wiki consolidate --workspace ${projectRoot}`,
|
|
252
467
|
];
|
|
@@ -345,6 +560,9 @@ export async function recordMaintenanceForEntry(projectRoot, entry, options = {}
|
|
|
345
560
|
if (!source && !options.resultMissing) return { created: false, reason: 'missing-source' };
|
|
346
561
|
if (!entry?.question || entry.question === '(not captured)') return { created: false, reason: 'missing-question' };
|
|
347
562
|
const created = nowIso();
|
|
563
|
+
const reason = sanitizeField(options.reason || `Captured ${options.eventName || 'turn'} needs durable wiki review.`, 300);
|
|
564
|
+
const signalLevel = normalizeSignalLevel(options.signalLevel) ||
|
|
565
|
+
inferSignalLevelFromReason(reason, Boolean(options.resultMissing));
|
|
348
566
|
const item = {
|
|
349
567
|
id: itemId(projectRoot, source, entry),
|
|
350
568
|
status: 'pending',
|
|
@@ -353,8 +571,9 @@ export async function recordMaintenanceForEntry(projectRoot, entry, options = {}
|
|
|
353
571
|
last_seen_at: created,
|
|
354
572
|
source,
|
|
355
573
|
suggested_target: options.suggestedTarget || inferSuggestedTarget(entry, source),
|
|
574
|
+
signal_level: signalLevel,
|
|
356
575
|
evidence_refs: evidenceRefsFromEntry(entry, { projectRoot }),
|
|
357
|
-
reason
|
|
576
|
+
reason,
|
|
358
577
|
result_missing: Boolean(options.resultMissing),
|
|
359
578
|
};
|
|
360
579
|
return appendMaintenanceItem(projectRoot, item);
|
|
@@ -395,6 +614,7 @@ export async function recoverStaleTurnStates(projectRoot, options = {}) {
|
|
|
395
614
|
eventName: 'recovered stale turn state',
|
|
396
615
|
reason: 'Recovered from stale turn state because Stop/SessionEnd did not complete.',
|
|
397
616
|
resultMissing: true,
|
|
617
|
+
signalLevel: 'recovered',
|
|
398
618
|
});
|
|
399
619
|
await unlink(path).catch(() => {});
|
|
400
620
|
output.push({ ...result, statePath: path, session: state.session });
|
|
@@ -469,6 +689,14 @@ export function formatMaintenanceResult(summary) {
|
|
|
469
689
|
if ((summary.reviewReasons || []).length > 0) {
|
|
470
690
|
lines.push(`- review reasons: ${summary.reviewReasons.join('; ')}`);
|
|
471
691
|
}
|
|
692
|
+
if ((summary.lifecycleActions || []).length > 0) {
|
|
693
|
+
lines.push(
|
|
694
|
+
`- lifecycle actions: ${summary.lifecycleActions.length}`,
|
|
695
|
+
`- lifecycle approved: ${summary.approvedByLifecycle || 0}`,
|
|
696
|
+
`- lifecycle skipped: ${summary.skippedByLifecycle || 0}`,
|
|
697
|
+
`- lifecycle archived reviewed: ${summary.archivedReviewedCount || 0}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
472
700
|
if ((summary.approved || []).length > 0) {
|
|
473
701
|
lines.push('', 'Approved:');
|
|
474
702
|
for (const item of summary.approved.slice(0, 10)) {
|
package/src/project.js
CHANGED
|
@@ -109,7 +109,27 @@ export async function writeQueryPage(projectRoot, entry) {
|
|
|
109
109
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'queries', `${day}-${slug}.md`);
|
|
110
110
|
if (await exists(path)) return path;
|
|
111
111
|
const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
|
|
112
|
-
const
|
|
112
|
+
const ko = entry.language === 'ko';
|
|
113
|
+
const labels = ko
|
|
114
|
+
? {
|
|
115
|
+
question: '질문',
|
|
116
|
+
answer: '답변 요약',
|
|
117
|
+
work: '작업 메모',
|
|
118
|
+
verification: '검증',
|
|
119
|
+
related: '관련 문서',
|
|
120
|
+
changeLog: '변경 기록',
|
|
121
|
+
captured: 'llm-wiki-kit hook이 자동 기록함.',
|
|
122
|
+
}
|
|
123
|
+
: {
|
|
124
|
+
question: 'Question',
|
|
125
|
+
answer: 'Answer Summary',
|
|
126
|
+
work: 'Work Notes',
|
|
127
|
+
verification: 'Verification',
|
|
128
|
+
related: 'Related Pages',
|
|
129
|
+
changeLog: 'Change Log',
|
|
130
|
+
captured: 'Captured automatically by llm-wiki-kit hook.',
|
|
131
|
+
};
|
|
132
|
+
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "query"\nsource_ids: []\n${evidenceRefs}\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\nmemory_type: "episodic"\nimportance: 2\nlast_verified: "unknown"\nsupersedes: []\nsuperseded_by: []\n---\n\n# ${entry.topic || entry.question.slice(0, 80)}\n\n## ${labels.question}\n${entry.question}\n\n## ${labels.answer}\n${entry.result || '(not captured)'}\n\n## ${labels.work}\n${entry.work || '(not captured)'}\n\n## ${labels.verification}\n${entry.verification || '(not captured)'}\n\n## ${labels.related}\n- [[index]]\n\n## ${labels.changeLog}\n- ${day}: ${labels.captured}\n`;
|
|
113
133
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
114
134
|
return path;
|
|
115
135
|
}
|
|
@@ -123,7 +143,33 @@ export async function writeDecisionPage(projectRoot, entry) {
|
|
|
123
143
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'decisions', `${day}-${slug}.md`);
|
|
124
144
|
if (await exists(path)) return path;
|
|
125
145
|
const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
|
|
126
|
-
const
|
|
146
|
+
const ko = entry.language === 'ko';
|
|
147
|
+
const labels = ko
|
|
148
|
+
? {
|
|
149
|
+
title: '결정',
|
|
150
|
+
decision: '결정',
|
|
151
|
+
context: '맥락',
|
|
152
|
+
evidence: '근거',
|
|
153
|
+
verification: '검증',
|
|
154
|
+
openQuestions: '열린 질문',
|
|
155
|
+
changeLog: '변경 기록',
|
|
156
|
+
fallbackDecision: '(assistant 응답에서 캡처됨; 검토 필요)',
|
|
157
|
+
fallbackOpen: '(기록 없음)',
|
|
158
|
+
captured: 'llm-wiki-kit hook이 자동 기록함.',
|
|
159
|
+
}
|
|
160
|
+
: {
|
|
161
|
+
title: 'Decision',
|
|
162
|
+
decision: 'Decision',
|
|
163
|
+
context: 'Context',
|
|
164
|
+
evidence: 'Evidence',
|
|
165
|
+
verification: 'Verification',
|
|
166
|
+
openQuestions: 'Open Questions',
|
|
167
|
+
changeLog: 'Change Log',
|
|
168
|
+
fallbackDecision: '(captured from assistant response; review needed)',
|
|
169
|
+
fallbackOpen: '(none captured)',
|
|
170
|
+
captured: 'Captured automatically by llm-wiki-kit hook.',
|
|
171
|
+
};
|
|
172
|
+
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "decision"\nsource_ids: []\n${evidenceRefs}\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\nmemory_type: "semantic"\nimportance: 4\nlast_verified: "unknown"\nsupersedes: []\nsuperseded_by: []\n---\n\n# ${entry.topic || labels.title}\n\n## ${labels.decision}\n${entry.result || labels.fallbackDecision}\n\n## ${labels.context}\n${entry.question || '(not captured)'}\n\n## ${labels.evidence}\n${entry.work || '(not captured)'}\n\n## ${labels.verification}\n${entry.verification || '(not captured)'}\n\n## ${labels.openQuestions}\n${entry.followUp || labels.fallbackOpen}\n\n## ${labels.changeLog}\n- ${day}: ${labels.captured}\n`;
|
|
127
173
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
128
174
|
return path;
|
|
129
175
|
}
|
package/src/state.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { unlink } from 'fs/promises';
|
|
3
3
|
import { kitDataDir, readJson, sha256, writeJson } from './fs-utils.js';
|
|
4
|
+
import { normalizeLanguage } from './language.js';
|
|
4
5
|
import { summarizeForStorage } from './redaction.js';
|
|
5
6
|
|
|
6
7
|
export function sessionKey(projectRoot, payload) {
|
|
@@ -17,6 +18,18 @@ export function statePath(projectRoot, payload) {
|
|
|
17
18
|
return join(kitDataDir(), 'state', `${sessionKey(projectRoot, payload)}.json`);
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function projectLanguageKey(projectRoot) {
|
|
22
|
+
return sha256(projectRoot).slice(0, 32);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sessionLanguagePath(projectRoot, payload) {
|
|
26
|
+
return join(kitDataDir(), 'language', `${sessionKey(projectRoot, payload)}.json`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function projectLanguagePath(projectRoot) {
|
|
30
|
+
return join(kitDataDir(), 'language', `${projectLanguageKey(projectRoot)}-project.json`);
|
|
31
|
+
}
|
|
32
|
+
|
|
20
33
|
export async function readTurnState(projectRoot, payload) {
|
|
21
34
|
return (await readJson(statePath(projectRoot, payload), null)) || {
|
|
22
35
|
projectRoot,
|
|
@@ -54,12 +67,40 @@ export async function clearCompactRecovery(projectRoot, payload) {
|
|
|
54
67
|
await unlink(compactRecoveryPath(projectRoot, payload)).catch(() => {});
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
export async function
|
|
70
|
+
export async function rememberLanguage(projectRoot, payload, language, source = 'prompt') {
|
|
71
|
+
const normalized = normalizeLanguage(language);
|
|
72
|
+
if (!normalized) return null;
|
|
73
|
+
const record = {
|
|
74
|
+
language: normalized,
|
|
75
|
+
source,
|
|
76
|
+
updated_at: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
await writeJson(sessionLanguagePath(projectRoot, payload), record);
|
|
79
|
+
await writeJson(projectLanguagePath(projectRoot), record);
|
|
80
|
+
return record;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function readRememberedLanguage(projectRoot, payload) {
|
|
84
|
+
const session = await readJson(sessionLanguagePath(projectRoot, payload), null);
|
|
85
|
+
const sessionLanguage = normalizeLanguage(session?.language);
|
|
86
|
+
if (sessionLanguage) return sessionLanguage;
|
|
87
|
+
const project = await readJson(projectLanguagePath(projectRoot), null);
|
|
88
|
+
return normalizeLanguage(project?.language);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function rememberQuestion(projectRoot, payload, prompt, options = {}) {
|
|
58
92
|
const state = await readTurnState(projectRoot, payload);
|
|
93
|
+
if (options.skipIfExisting && state.questions.length > 0) {
|
|
94
|
+
if (options.language) state.language = normalizeLanguage(options.language) || state.language;
|
|
95
|
+
await writeTurnState(projectRoot, payload, state);
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
59
98
|
const clean = summarizeForStorage(prompt, 3000);
|
|
60
99
|
if (clean) state.questions.push({ at: new Date().toISOString(), text: clean });
|
|
61
100
|
state.questions = state.questions.slice(-5);
|
|
101
|
+
if (options.language) state.language = normalizeLanguage(options.language) || state.language;
|
|
62
102
|
await writeTurnState(projectRoot, payload, state);
|
|
103
|
+
return state;
|
|
63
104
|
}
|
|
64
105
|
|
|
65
106
|
export async function rememberTool(projectRoot, payload, summary) {
|
|
@@ -112,5 +153,6 @@ export function buildEntryFromTurnState(state, assistantText = '') {
|
|
|
112
153
|
followUp: '',
|
|
113
154
|
firstTimestamp: questions[0]?.at || tools[0]?.at || state.updated_at,
|
|
114
155
|
session: state.session,
|
|
156
|
+
language: normalizeLanguage(state.language),
|
|
115
157
|
};
|
|
116
158
|
}
|
package/src/templates.js
CHANGED
|
@@ -9,14 +9,14 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
|
|
|
9
9
|
|
|
10
10
|
- This block supersedes older OMX/OMC/\`omx_wiki/\` LLM Wiki instructions for this repository.
|
|
11
11
|
- Use Codex or Claude Code normally. Do not create a workflow where users must remember extra \`llm-wiki\` commands during ordinary work.
|
|
12
|
-
- Language: respond in the user's current prompt language when it is clearly Korean or English. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
|
|
12
|
+
- Language: respond in the user's current real prompt language when it is clearly Korean or English. Codex synthetic plan execution prompts such as \`Implement the plan.\` are neutral and should reuse the last real session language or project preference. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
|
|
13
13
|
- Treat chat memory as temporary. Durable project knowledge belongs in repository Markdown under \`llm-wiki/\`.
|
|
14
14
|
- \`llm-wiki/raw/\` is the immutable or redacted evidence layer. Do not modify raw captures except safe hook envelope append.
|
|
15
15
|
- \`llm-wiki/wiki/\` is the curated knowledge layer managed by the agent. Put decisions, architecture, debugging findings, concepts, procedures, and context there.
|
|
16
16
|
- Keep \`llm-wiki/wiki/memory.md\` short. Link to important documents instead of copying long explanations.
|
|
17
17
|
- Use hook-injected context when helpful, but answer the current user request first. Use \`llm-wiki context\`, \`llm-wiki lint\`, and \`llm-wiki consolidate\` only as agent maintenance helpers.
|
|
18
18
|
- Hooks safely store redacted raw envelopes and work/decision-focused live Q&A. Simple answers, status checks, and keyword-only turns should not be promoted to live Q&A or durable wiki by default.
|
|
19
|
-
- Hooks may queue durable cleanup candidates in \`llm-wiki/outputs/maintenance/queue.md\` at stop/start boundaries. Treat maintenance as a soft agent-side reminder; merge surfaced candidates when relevant or before ending the session, then mark them \`done\` or \`skipped\`.
|
|
19
|
+
- Hooks may queue durable cleanup candidates in \`llm-wiki/outputs/maintenance/queue.md\` at stop/start boundaries. Session-start lifecycle may move explicit/high-signal candidates to \`approved\`, skip stale low-signal candidates, and archive old reviewed items, but it never creates durable wiki pages automatically. Treat maintenance as a soft agent-side reminder; merge surfaced candidates when relevant or before ending the session, then mark them \`done\` or \`skipped\`.
|
|
20
20
|
- Before creating new wiki pages, search existing pages and update the right one when possible. Reusable facts should not stay only in chunked \`outputs/questions/\`.
|
|
21
21
|
- Store one-off work or decision records in \`llm-wiki/outputs/questions/YYYY-MM-DD/live-qa-001.md\` when needed. Merge reusable knowledge into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` when approved, hook-suggested, or clearly important.
|
|
22
22
|
- Record verification commands, evidence files, and uncertainty. Mark inference explicitly and preserve contradictions in Open Questions or Contradictions.
|
|
@@ -46,7 +46,8 @@ The user should not need to run many \`llm-wiki\` commands manually. The agent c
|
|
|
46
46
|
These rules replace older OMX/OMC/\`omx_wiki/\` rules for this project.
|
|
47
47
|
|
|
48
48
|
## Language
|
|
49
|
-
- Follow the user's current prompt language when it is clearly Korean or English.
|
|
49
|
+
- Follow the user's current real prompt language when it is clearly Korean or English.
|
|
50
|
+
- Treat Codex synthetic plan execution prompts such as \`Implement the plan.\` as neutral; reuse the last real session language or project preference when available.
|
|
50
51
|
- If no current prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
|
|
51
52
|
- Keep commands, paths, code identifiers, API names, package names, logs, and original error text unchanged.
|
|
52
53
|
- Korean and English users should both be able to use Codex/Claude Code naturally without changing \`llm-wiki\` commands.
|
|
@@ -66,8 +67,8 @@ These rules replace older OMX/OMC/\`omx_wiki/\` rules for this project.
|
|
|
66
67
|
- Do not promote simple answers, status checks, keyword-only replies, or one-off chat into live Q&A, \`wiki/queries\`, or maintenance.
|
|
67
68
|
- Hooks may flag work/decision turns as durable wiki candidates when they contain reusable architecture, debugging, policy, procedure, or decision signals. Treat those as review prompts, not automatic page creation.
|
|
68
69
|
- Merge reusable knowledge into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` based on importance, hook suggestions, and user consent flow.
|
|
69
|
-
- Review \`outputs/maintenance/queue.md\` pending items when surfaced by hook context, related to the current request, or review is due. Merge into existing durable wiki pages, then mark items \`done\` or \`skipped\`.
|
|
70
|
-
- Periodic maintenance is agent review, not automatic
|
|
70
|
+
- Review \`outputs/maintenance/queue.md\` pending or approved items when surfaced by hook context, related to the current request, or review is due. Merge into existing durable wiki pages, then mark items \`done\` or \`skipped\`.
|
|
71
|
+
- Periodic maintenance is agent review, not automatic page creation. Session-start lifecycle may approve explicit/high-signal items, skip stale low-signal items, and archive old reviewed items. Show only short reminders at \`SessionStart\`/\`InstructionsLoaded\`, and compact prompt-time reminders when durable candidates or review thresholds need agent attention.
|
|
71
72
|
- Keep \`wiki/memory.md\` short. Use links to current state and important documents instead of long explanations.
|
|
72
73
|
- Preserve contradictions in \`Contradictions\` or \`Open Questions\`; do not overwrite them silently.
|
|
73
74
|
- Do not store credentials, tokens, passwords, private keys, or raw \`.env\` contents in wiki.
|
|
@@ -106,7 +107,7 @@ superseded_by: []
|
|
|
106
107
|
- query: use hook-injected context when useful, but answer the current request first. Manual commands are diagnostics, not required daily workflow.
|
|
107
108
|
- lint: agent helper for user-requested or wiki maintenance work. Do not run it every turn.
|
|
108
109
|
- consolidate: safely refresh generated blocks in \`memory.md\`/\`index.md\`. Do not overwrite handwritten sections or curated document bodies.
|
|
109
|
-
- maintenance: \`outputs/maintenance/queue.md\` stores stop/start cleanup candidates. Review surfaced durable candidates without delaying the current user request, then mark them \`done\` or \`skipped\`.
|
|
110
|
+
- maintenance: \`outputs/maintenance/queue.md\` stores stop/start cleanup candidates. Review surfaced durable candidates without delaying the current user request, then mark them \`done\` or \`skipped\`. Use \`llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run\` when queue hygiene needs inspection.
|
|
110
111
|
`;
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -225,9 +226,10 @@ Periodic maintenance is an agent-side task, not a per-turn user command. When ne
|
|
|
225
226
|
|
|
226
227
|
1. \`llm-wiki lint --workspace <project>\`
|
|
227
228
|
2. \`llm-wiki maintenance --workspace <project>\`
|
|
228
|
-
3.
|
|
229
|
-
4.
|
|
230
|
-
5.
|
|
229
|
+
3. \`llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run\`
|
|
230
|
+
4. Merge pending, approved, or hook-suggested durable candidates into existing durable pages when appropriate, then mark them \`done\` or \`skipped\`.
|
|
231
|
+
5. \`llm-wiki consolidate --workspace <project> --dry-run\`
|
|
232
|
+
6. Run \`llm-wiki consolidate --workspace <project>\` when the dry run is acceptable.
|
|
231
233
|
|
|
232
234
|
Apply automatic fixes only to clearly managed kit areas. Do not overwrite user-editable documents; surface cleanup needs in the next work context.
|
|
233
235
|
`,
|