llm-wiki-kit 0.2.14 → 0.2.16
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 +17 -9
- package/docs/concepts.md +15 -10
- package/docs/integrations/claude-code.md +4 -2
- package/docs/integrations/codex.md +6 -4
- package/docs/manual.md +55 -7
- package/docs/operations.md +2 -2
- package/docs/security.md +4 -0
- package/docs/troubleshooting.md +2 -2
- package/package.json +1 -1
- package/src/capture-policy.js +22 -4
- package/src/cli.js +52 -2
- package/src/constants.js +2 -0
- package/src/evidence.js +128 -0
- package/src/hook.js +18 -4
- package/src/maintenance.js +142 -22
- package/src/project-state.js +9 -2
- package/src/project.js +5 -2
- package/src/templates.js +31 -10
- package/src/wiki-eval.js +110 -0
- package/src/wiki-export.js +214 -0
- package/src/wiki-lint.js +66 -1
- package/src/wiki-model.js +2 -0
- package/src/wiki-search.js +79 -13
- package/src/wiki-visibility.js +34 -4
package/src/evidence.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from 'path';
|
|
2
|
+
import { extractPathsFromText, hasSecretLikeText, isSensitivePath, redactText, summarizeForStorage } from './redaction.js';
|
|
3
|
+
|
|
4
|
+
export const EVIDENCE_PREFIXES = new Set(['file', 'cmd', 'raw', 'url']);
|
|
5
|
+
export const MAX_CMD_EVIDENCE_CHARS = 500;
|
|
6
|
+
|
|
7
|
+
export function normalizeEvidenceRefs(value) {
|
|
8
|
+
if (!Array.isArray(value)) return [];
|
|
9
|
+
const refs = value
|
|
10
|
+
.map((item) => summarizeForStorage(String(item || ''), 700))
|
|
11
|
+
.map((item) => item.replace(/\s+/g, ' ').trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
return [...new Set(refs)];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseEvidenceRef(ref) {
|
|
17
|
+
const text = String(ref || '').trim();
|
|
18
|
+
const match = text.match(/^([a-z]+):([\s\S]*)$/i);
|
|
19
|
+
if (!match) return { raw: text, prefix: '', value: text };
|
|
20
|
+
return {
|
|
21
|
+
raw: text,
|
|
22
|
+
prefix: match[1].toLowerCase(),
|
|
23
|
+
value: match[2].trim(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseEvidenceRefsField(value) {
|
|
28
|
+
const text = String(value || '').trim();
|
|
29
|
+
if (!text) return [];
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(text);
|
|
32
|
+
if (Array.isArray(parsed)) return normalizeEvidenceRefs(parsed);
|
|
33
|
+
} catch {
|
|
34
|
+
// Fall through to legacy comma/text parsing.
|
|
35
|
+
}
|
|
36
|
+
return normalizeEvidenceRefs(text.split(',').map((item) => item.trim()));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function frontmatterEvidenceRefs(refs) {
|
|
40
|
+
const normalized = normalizeEvidenceRefs(refs);
|
|
41
|
+
if (normalized.length === 0) return 'evidence_refs: []';
|
|
42
|
+
return ['evidence_refs:', ...normalized.map((ref) => ` - "${ref.replace(/"/g, '\\"')}"`)].join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cleanCandidatePath(value) {
|
|
46
|
+
return String(value || '')
|
|
47
|
+
.replace(/^[-*]\s+/, '')
|
|
48
|
+
.replace(/^file:/i, '')
|
|
49
|
+
.replace(/[),.;:]+$/g, '')
|
|
50
|
+
.replace(/\\/g, '/')
|
|
51
|
+
.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isUsefulFileCandidate(value) {
|
|
55
|
+
const text = cleanCandidatePath(value);
|
|
56
|
+
if (!text || text.includes('://') || text.startsWith('cmd:') || text.startsWith('raw:')) return false;
|
|
57
|
+
if (isSensitivePath(text) || hasSecretLikeText(text)) return false;
|
|
58
|
+
return (
|
|
59
|
+
text.startsWith('llm-wiki/') ||
|
|
60
|
+
text.startsWith('src/') ||
|
|
61
|
+
text.startsWith('test/') ||
|
|
62
|
+
text.startsWith('docs/') ||
|
|
63
|
+
text.startsWith('bin/') ||
|
|
64
|
+
text.startsWith('examples/') ||
|
|
65
|
+
/^(?:README|AGENTS|CLAUDE|LICENSE|package(?:-lock)?|install)\.[A-Za-z0-9]+$/i.test(text) ||
|
|
66
|
+
/\.[A-Za-z0-9]{1,12}$/.test(text)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function relativeProjectPath(projectRoot, value) {
|
|
71
|
+
const cleaned = cleanCandidatePath(value);
|
|
72
|
+
if (!cleaned) return '';
|
|
73
|
+
if (projectRoot && cleaned.startsWith(projectRoot)) {
|
|
74
|
+
return relative(projectRoot, cleaned).split(sep).join('/');
|
|
75
|
+
}
|
|
76
|
+
return cleaned.replace(/^\.\//, '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function addFileRefs(refs, projectRoot, text) {
|
|
80
|
+
for (const candidate of extractPathsFromText(text || '')) {
|
|
81
|
+
const rel = relativeProjectPath(projectRoot, candidate);
|
|
82
|
+
if (!isUsefulFileCandidate(rel)) continue;
|
|
83
|
+
if (isAbsolute(rel) || rel.split('/').includes('..')) continue;
|
|
84
|
+
refs.push(`file:${rel}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function decodeJsonString(value) {
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(`"${value}"`);
|
|
91
|
+
} catch {
|
|
92
|
+
return value.replace(/\\"/g, '"');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addCommandRefs(refs, text) {
|
|
97
|
+
const body = String(text || '');
|
|
98
|
+
const cmdRegex = /"cmd"\s*:\s*"((?:\\.|[^"\\])*)"/g;
|
|
99
|
+
let match = cmdRegex.exec(body);
|
|
100
|
+
while (match) {
|
|
101
|
+
const command = summarizeForStorage(decodeJsonString(match[1]).replace(/\s+/g, ' '), MAX_CMD_EVIDENCE_CHARS);
|
|
102
|
+
if (command && !hasSecretLikeText(command)) refs.push(`cmd:${command}`);
|
|
103
|
+
match = cmdRegex.exec(body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const line of body.split(/\r?\n/)) {
|
|
107
|
+
const cleaned = line.replace(/^[-*]\s+/, '').trim();
|
|
108
|
+
const direct = cleaned.match(/^(?:Bash|Shell|Verification|Command):\s*(.+)$/i)?.[1];
|
|
109
|
+
const command = direct || (/^(?:node|npm|npx|git|llm-wiki|pytest|python|pnpm|yarn|vitest|jest|tsc)\b/.test(cleaned) ? cleaned : '');
|
|
110
|
+
if (!command) continue;
|
|
111
|
+
const safe = summarizeForStorage(command.replace(/\s+/g, ' '), MAX_CMD_EVIDENCE_CHARS);
|
|
112
|
+
if (safe && !hasSecretLikeText(safe)) refs.push(`cmd:${safe}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function evidenceRefsFromEntry(entry, options = {}) {
|
|
117
|
+
const refs = [];
|
|
118
|
+
const projectRoot = options.projectRoot || '';
|
|
119
|
+
addFileRefs(refs, projectRoot, entry?.changedFiles);
|
|
120
|
+
addFileRefs(refs, projectRoot, entry?.work);
|
|
121
|
+
addCommandRefs(refs, entry?.verification);
|
|
122
|
+
addCommandRefs(refs, entry?.work);
|
|
123
|
+
return normalizeEvidenceRefs(refs).slice(0, options.limit || 20);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function redactEvidenceRefs(refs) {
|
|
127
|
+
return normalizeEvidenceRefs(refs).map((ref) => redactText(ref, 700));
|
|
128
|
+
}
|
package/src/hook.js
CHANGED
|
@@ -81,21 +81,35 @@ async function handleLegacyEagerStop(projectRoot, eventName, payload, entry) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function durableQueueReason(classification) {
|
|
85
|
+
if (classification.kind === 'explicit-durable') {
|
|
86
|
+
return 'Explicit durable documentation request did not have a detected durable wiki update.';
|
|
87
|
+
}
|
|
88
|
+
return 'Suggested durable wiki candidate did not have a detected durable wiki update.';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function durableFollowUp(entry, classification) {
|
|
92
|
+
if (!classification.suggestDurable) return entry.followUp;
|
|
93
|
+
const message = 'This turn is a durable wiki candidate. After the current answer, merge reusable facts into an existing durable wiki page or mark the maintenance item skipped.';
|
|
94
|
+
const current = String(entry.followUp || '').trim();
|
|
95
|
+
if (!current || current === '(not captured)') return message;
|
|
96
|
+
if (current.includes(message)) return current;
|
|
97
|
+
return `${current}\n${message}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
async function handleAnswerFirstStop(projectRoot, eventName, payload, entry) {
|
|
85
101
|
const classification = classifyTurn(entry, eventName);
|
|
86
102
|
if (classification.archive) {
|
|
87
103
|
const archiveEntry = {
|
|
88
104
|
...entry,
|
|
89
|
-
followUp: classification
|
|
90
|
-
? 'This turn may be worth preserving. If the user approves, merge it into an existing durable wiki page.'
|
|
91
|
-
: entry.followUp,
|
|
105
|
+
followUp: durableFollowUp(entry, classification),
|
|
92
106
|
};
|
|
93
107
|
const liveQaPath = await appendLiveQa(projectRoot, archiveEntry);
|
|
94
108
|
if (classification.queueIfMissingDurable && !hasDetectedDurableWikiChange(entry)) {
|
|
95
109
|
await recordMaintenanceForEntry(projectRoot, entry, {
|
|
96
110
|
source: liveQaPath,
|
|
97
111
|
eventName,
|
|
98
|
-
reason:
|
|
112
|
+
reason: durableQueueReason(classification),
|
|
99
113
|
}).catch(() => {});
|
|
100
114
|
}
|
|
101
115
|
await appendWikiLog(projectRoot, `captured ${eventName}; archive=${relative(projectRoot, liveQaPath)}; classification=${classification.kind}`);
|
package/src/maintenance.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, unlink } from 'fs/promises';
|
|
2
2
|
import { join, relative } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { evidenceRefsFromEntry, normalizeEvidenceRefs, parseEvidenceRefsField } from './evidence.js';
|
|
4
|
+
import { appendText, exists, kitDataDir, readJson, readText, sha256, writeText, writeTextIfMissing } from './fs-utils.js';
|
|
4
5
|
import { classifyTurn, isMaintenanceRelatedQuery } from './capture-policy.js';
|
|
5
6
|
import { redactText, summarizeForStorage } from './redaction.js';
|
|
6
7
|
import { buildEntryFromTurnState, hasRecoverableTurnState } from './state.js';
|
|
@@ -24,7 +25,7 @@ function queueHeader() {
|
|
|
24
25
|
'',
|
|
25
26
|
'Candidates to merge into durable wiki pages. Hooks only create candidates; the active agent reviews and merges them into existing durable wiki documents.',
|
|
26
27
|
'',
|
|
27
|
-
'Status values: pending, done, skipped.',
|
|
28
|
+
'Status values: pending, approved, done, skipped.',
|
|
28
29
|
'',
|
|
29
30
|
].join('\n');
|
|
30
31
|
}
|
|
@@ -70,18 +71,51 @@ function itemId(projectRoot, source, entry) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
function itemBlock(item) {
|
|
73
|
-
|
|
74
|
+
const fields = {
|
|
75
|
+
...(item.fields || {}),
|
|
76
|
+
id: item.id,
|
|
77
|
+
created_at: item.created_at,
|
|
78
|
+
last_seen_at: item.last_seen_at,
|
|
79
|
+
source: item.source,
|
|
80
|
+
suggested_target: item.suggested_target,
|
|
81
|
+
target: item.target,
|
|
82
|
+
reviewed_at: item.reviewed_at,
|
|
83
|
+
review_note: item.review_note,
|
|
84
|
+
evidence_refs: normalizeEvidenceRefs(item.evidence_refs).length > 0
|
|
85
|
+
? JSON.stringify(normalizeEvidenceRefs(item.evidence_refs))
|
|
86
|
+
: item.fields?.evidence_refs,
|
|
87
|
+
reason: item.reason,
|
|
88
|
+
result_missing: item.result_missing ? 'true' : 'false',
|
|
89
|
+
};
|
|
90
|
+
const order = [
|
|
91
|
+
'id',
|
|
92
|
+
'created_at',
|
|
93
|
+
'last_seen_at',
|
|
94
|
+
'source',
|
|
95
|
+
'suggested_target',
|
|
96
|
+
'target',
|
|
97
|
+
'reviewed_at',
|
|
98
|
+
'review_note',
|
|
99
|
+
'evidence_refs',
|
|
100
|
+
'reason',
|
|
101
|
+
'result_missing',
|
|
102
|
+
];
|
|
103
|
+
const lines = [
|
|
74
104
|
`## ${item.status || 'pending'} - ${item.topic || 'maintenance item'}`,
|
|
75
105
|
'',
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
];
|
|
107
|
+
const emitted = new Set();
|
|
108
|
+
for (const key of order) {
|
|
109
|
+
if (fields[key] === undefined || fields[key] === '') continue;
|
|
110
|
+
lines.push(`- ${key}: ${fields[key]}`);
|
|
111
|
+
emitted.add(key);
|
|
112
|
+
}
|
|
113
|
+
for (const key of Object.keys(fields).sort()) {
|
|
114
|
+
if (emitted.has(key) || fields[key] === undefined || fields[key] === '') continue;
|
|
115
|
+
lines.push(`- ${key}: ${fields[key]}`);
|
|
116
|
+
}
|
|
117
|
+
lines.push('');
|
|
118
|
+
return lines.join('\n');
|
|
85
119
|
}
|
|
86
120
|
|
|
87
121
|
export async function readMaintenanceQueue(projectRoot) {
|
|
@@ -96,7 +130,7 @@ export async function readMaintenanceQueue(projectRoot) {
|
|
|
96
130
|
};
|
|
97
131
|
|
|
98
132
|
for (const line of text.split(/\r?\n/)) {
|
|
99
|
-
const header = line.match(/^##\s+(pending|done|skipped)\s+-\s*(.*)$/);
|
|
133
|
+
const header = line.match(/^##\s+(pending|approved|done|skipped)\s+-\s*(.*)$/);
|
|
100
134
|
if (header) {
|
|
101
135
|
pushCurrent();
|
|
102
136
|
current = {
|
|
@@ -107,7 +141,7 @@ export async function readMaintenanceQueue(projectRoot) {
|
|
|
107
141
|
continue;
|
|
108
142
|
}
|
|
109
143
|
if (!current) continue;
|
|
110
|
-
const field = line.match(/^-\s+([
|
|
144
|
+
const field = line.match(/^-\s+([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
111
145
|
if (field) {
|
|
112
146
|
current.fields[field[1]] = field[2].trim();
|
|
113
147
|
}
|
|
@@ -119,19 +153,35 @@ export async function readMaintenanceQueue(projectRoot) {
|
|
|
119
153
|
exists: await exists(path),
|
|
120
154
|
items: items.map((item) => ({
|
|
121
155
|
...item.fields,
|
|
156
|
+
fields: item.fields,
|
|
122
157
|
status: item.status,
|
|
123
158
|
topic: item.topic,
|
|
159
|
+
evidence_refs: parseEvidenceRefsField(item.fields.evidence_refs),
|
|
160
|
+
evidenceRefs: parseEvidenceRefsField(item.fields.evidence_refs),
|
|
124
161
|
result_missing: String(item.fields.result_missing || '').toLowerCase() === 'true',
|
|
125
162
|
})),
|
|
126
163
|
};
|
|
127
164
|
}
|
|
128
165
|
|
|
166
|
+
async function writeMaintenanceQueue(projectRoot, items) {
|
|
167
|
+
const content = [
|
|
168
|
+
queueHeader().trimEnd(),
|
|
169
|
+
'',
|
|
170
|
+
...items.map((item) => itemBlock(item).trimEnd()),
|
|
171
|
+
'',
|
|
172
|
+
].join('\n');
|
|
173
|
+
const path = queuePath(projectRoot);
|
|
174
|
+
await writeText(path, content);
|
|
175
|
+
return path;
|
|
176
|
+
}
|
|
177
|
+
|
|
129
178
|
export function summarizeMaintenanceQueue(queue, options = {}) {
|
|
130
179
|
const staleDays = options.staleDays ?? DEFAULT_STALE_PENDING_DAYS;
|
|
131
180
|
const pendingLimit = options.pendingLimit ?? DEFAULT_PENDING_LIMIT;
|
|
132
181
|
const reviewPendingLimit = options.reviewPendingLimit ?? DEFAULT_REVIEW_PENDING_LIMIT;
|
|
133
182
|
const reviewIntervalDays = options.reviewIntervalDays ?? DEFAULT_REVIEW_INTERVAL_DAYS;
|
|
134
183
|
const pending = queue.items.filter((item) => item.status === 'pending');
|
|
184
|
+
const approved = queue.items.filter((item) => item.status === 'approved');
|
|
135
185
|
const staleCutoff = Date.now() - staleDays * 24 * 60 * 60 * 1000;
|
|
136
186
|
const stalePending = pending.filter((item) => {
|
|
137
187
|
const time = Date.parse(item.created_at || item.last_seen_at || '');
|
|
@@ -143,6 +193,7 @@ export function summarizeMaintenanceQueue(queue, options = {}) {
|
|
|
143
193
|
.filter(Number.isFinite);
|
|
144
194
|
const lastReviewMs = reviewTimes.length > 0 ? Math.max(...reviewTimes) : null;
|
|
145
195
|
const reviewReasons = [];
|
|
196
|
+
if (approved.length > 0) reviewReasons.push(`approved queue has ${approved.length} item(s) ready for durable wiki merge`);
|
|
146
197
|
if (pending.length >= reviewPendingLimit) reviewReasons.push(`pending queue has ${pending.length} items (threshold ${reviewPendingLimit})`);
|
|
147
198
|
if (stalePending.length > 0) reviewReasons.push(`${stalePending.length} pending item(s) older than ${staleDays} days`);
|
|
148
199
|
if (pending.some((item) => item.result_missing)) reviewReasons.push('pending recovered turn state needs review');
|
|
@@ -153,9 +204,11 @@ export function summarizeMaintenanceQueue(queue, options = {}) {
|
|
|
153
204
|
path: queue.path,
|
|
154
205
|
exists: queue.exists,
|
|
155
206
|
items: queue.items,
|
|
207
|
+
approved,
|
|
156
208
|
pending,
|
|
157
209
|
done: queue.items.filter((item) => item.status === 'done'),
|
|
158
210
|
skipped: queue.items.filter((item) => item.status === 'skipped'),
|
|
211
|
+
approvedCount: approved.length,
|
|
159
212
|
pendingCount: pending.length,
|
|
160
213
|
doneCount: queue.items.filter((item) => item.status === 'done').length,
|
|
161
214
|
skippedCount: queue.items.filter((item) => item.status === 'skipped').length,
|
|
@@ -246,6 +299,47 @@ export async function appendMaintenanceItem(projectRoot, item) {
|
|
|
246
299
|
return { created: true, path, id: item.id };
|
|
247
300
|
}
|
|
248
301
|
|
|
302
|
+
export async function updateMaintenanceItem(projectRoot, id, action, options = {}) {
|
|
303
|
+
const allowed = new Set(['approve', 'done', 'skip']);
|
|
304
|
+
if (!allowed.has(action)) throw new Error(`unsupported maintenance action: ${action}`);
|
|
305
|
+
const queue = await readMaintenanceQueue(projectRoot);
|
|
306
|
+
const index = queue.items.findIndex((item) => item.id === id);
|
|
307
|
+
if (index === -1) throw new Error(`maintenance item not found: ${id}`);
|
|
308
|
+
if ((action === 'approve' || action === 'done') && !options.target && !queue.items[index].target) {
|
|
309
|
+
throw new Error(`--target is required for maintenance --${action}`);
|
|
310
|
+
}
|
|
311
|
+
const now = nowIso();
|
|
312
|
+
const current = queue.items[index];
|
|
313
|
+
const next = {
|
|
314
|
+
...current,
|
|
315
|
+
fields: { ...(current.fields || {}) },
|
|
316
|
+
status: action === 'approve' ? 'approved' : (action === 'done' ? 'done' : 'skipped'),
|
|
317
|
+
last_seen_at: now,
|
|
318
|
+
reviewed_at: now,
|
|
319
|
+
target: options.target || current.target || '',
|
|
320
|
+
review_note: sanitizeField(options.note || current.review_note || '', 500),
|
|
321
|
+
};
|
|
322
|
+
if (next.target) next.fields.target = next.target;
|
|
323
|
+
next.fields.last_seen_at = next.last_seen_at;
|
|
324
|
+
next.fields.reviewed_at = next.reviewed_at;
|
|
325
|
+
if (next.review_note) next.fields.review_note = next.review_note;
|
|
326
|
+
const refs = normalizeEvidenceRefs(options.evidence_refs || current.evidence_refs || current.evidenceRefs || []);
|
|
327
|
+
if (refs.length > 0) {
|
|
328
|
+
next.evidence_refs = refs;
|
|
329
|
+
next.fields.evidence_refs = JSON.stringify(refs);
|
|
330
|
+
}
|
|
331
|
+
queue.items[index] = next;
|
|
332
|
+
await writeMaintenanceQueue(projectRoot, queue.items);
|
|
333
|
+
return {
|
|
334
|
+
workspace: projectRoot,
|
|
335
|
+
path: queue.path,
|
|
336
|
+
id,
|
|
337
|
+
status: next.status,
|
|
338
|
+
target: next.target || null,
|
|
339
|
+
reviewed_at: next.reviewed_at,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
249
343
|
export async function recordMaintenanceForEntry(projectRoot, entry, options = {}) {
|
|
250
344
|
const source = relativeSource(projectRoot, options.source || options.queryPath || options.decisionPath || options.liveQaPath || '');
|
|
251
345
|
if (!source && !options.resultMissing) return { created: false, reason: 'missing-source' };
|
|
@@ -259,6 +353,7 @@ export async function recordMaintenanceForEntry(projectRoot, entry, options = {}
|
|
|
259
353
|
last_seen_at: created,
|
|
260
354
|
source,
|
|
261
355
|
suggested_target: options.suggestedTarget || inferSuggestedTarget(entry, source),
|
|
356
|
+
evidence_refs: evidenceRefsFromEntry(entry, { projectRoot }),
|
|
262
357
|
reason: sanitizeField(options.reason || `Captured ${options.eventName || 'turn'} needs durable wiki review.`, 300),
|
|
263
358
|
result_missing: Boolean(options.resultMissing),
|
|
264
359
|
};
|
|
@@ -307,37 +402,55 @@ export async function recoverStaleTurnStates(projectRoot, options = {}) {
|
|
|
307
402
|
return output;
|
|
308
403
|
}
|
|
309
404
|
|
|
405
|
+
function isDurablePromotionCandidate(item) {
|
|
406
|
+
if (item?.status === 'approved') return true;
|
|
407
|
+
const text = `${item?.reason || ''} ${item?.fields?.reason || ''}`.toLowerCase();
|
|
408
|
+
return /suggested durable wiki candidate|precompact durable candidate|explicit durable documentation|durable wiki review/.test(text);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function shouldSurfacePromptMaintenance(summary, query, reviewCandidates) {
|
|
412
|
+
if (isMaintenanceRelatedQuery(query || '', reviewCandidates)) return true;
|
|
413
|
+
if ((summary.approvedCount || 0) > 0) return true;
|
|
414
|
+
if (reviewCandidates.some(isDurablePromotionCandidate)) return true;
|
|
415
|
+
if ((summary.pendingCount || 0) >= (summary.reviewPendingLimit || DEFAULT_REVIEW_PENDING_LIMIT)) return true;
|
|
416
|
+
if ((summary.stalePendingCount || 0) > 0) return true;
|
|
417
|
+
return reviewCandidates.some((item) => item.result_missing);
|
|
418
|
+
}
|
|
419
|
+
|
|
310
420
|
export function formatMaintenanceContext(summary, options = {}) {
|
|
311
421
|
if (!summary.reviewDue) return '';
|
|
312
422
|
const language = options.language === 'ko' ? 'ko' : 'en';
|
|
313
423
|
const eventName = options.eventName || '';
|
|
314
424
|
const defaultLimit = eventName === 'SessionStart' || eventName === 'InstructionsLoaded' ? 1 : 5;
|
|
315
425
|
const limit = options.limit || defaultLimit;
|
|
316
|
-
|
|
426
|
+
const reviewCandidates = [...(summary.approved || []), ...(summary.pending || [])];
|
|
427
|
+
let pending = reviewCandidates.slice(0, limit);
|
|
317
428
|
|
|
318
429
|
if (eventName === 'UserPromptSubmit') {
|
|
319
|
-
if (!
|
|
320
|
-
pending =
|
|
430
|
+
if (!shouldSurfacePromptMaintenance(summary, options.query || '', reviewCandidates)) return '';
|
|
431
|
+
pending = reviewCandidates.slice(0, 1);
|
|
321
432
|
} else if (eventName !== 'SessionStart' && eventName !== 'InstructionsLoaded') {
|
|
322
433
|
return '';
|
|
323
434
|
}
|
|
324
435
|
|
|
436
|
+
const reviewCount = (summary.approvedCount || 0) + (summary.pendingCount || 0);
|
|
325
437
|
const lines = language === 'ko'
|
|
326
438
|
? [
|
|
327
439
|
'LLM Wiki maintenance status:',
|
|
328
440
|
`- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
|
|
329
|
-
`- pending review items: ${
|
|
441
|
+
`- approved/pending review items: ${reviewCount}. 현재 요청이 우선이며, 관련 있거나 세션 종료 전 여유가 있으면 durable wiki에 병합하거나 skipped로 표시한다.`,
|
|
330
442
|
]
|
|
331
443
|
: [
|
|
332
444
|
'LLM Wiki maintenance status:',
|
|
333
445
|
`- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
|
|
334
|
-
`- pending review items: ${
|
|
446
|
+
`- approved/pending review items: ${reviewCount}. The current request comes first; when relevant or before ending the session, merge candidates into durable wiki pages or mark them skipped.`,
|
|
335
447
|
];
|
|
336
448
|
for (const item of pending) {
|
|
337
|
-
|
|
449
|
+
const target = item.target || item.suggested_target;
|
|
450
|
+
lines.push(`- ${item.status}: ${item.topic || item.id}: ${target}; source=${item.source}${item.result_missing ? '; result missing' : ''}`);
|
|
338
451
|
}
|
|
339
|
-
if (
|
|
340
|
-
lines.push(`- ${
|
|
452
|
+
if (reviewCandidates.length > pending.length) {
|
|
453
|
+
lines.push(`- ${reviewCandidates.length - pending.length} more review item(s) available in llm-wiki/outputs/maintenance/queue.md.`);
|
|
341
454
|
}
|
|
342
455
|
return lines.join('\n');
|
|
343
456
|
}
|
|
@@ -346,6 +459,7 @@ export function formatMaintenanceResult(summary) {
|
|
|
346
459
|
const lines = [
|
|
347
460
|
'llm-wiki maintenance',
|
|
348
461
|
`- queue: ${summary.path}`,
|
|
462
|
+
`- approved: ${summary.approvedCount || 0}`,
|
|
349
463
|
`- pending: ${summary.pendingCount}`,
|
|
350
464
|
`- stale pending: ${summary.stalePendingCount || 0}`,
|
|
351
465
|
`- done: ${summary.doneCount}`,
|
|
@@ -355,6 +469,12 @@ export function formatMaintenanceResult(summary) {
|
|
|
355
469
|
if ((summary.reviewReasons || []).length > 0) {
|
|
356
470
|
lines.push(`- review reasons: ${summary.reviewReasons.join('; ')}`);
|
|
357
471
|
}
|
|
472
|
+
if ((summary.approved || []).length > 0) {
|
|
473
|
+
lines.push('', 'Approved:');
|
|
474
|
+
for (const item of summary.approved.slice(0, 10)) {
|
|
475
|
+
lines.push(`- ${item.topic || item.id}: ${item.source} -> ${item.target || item.suggested_target}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
358
478
|
if (summary.pending.length > 0) {
|
|
359
479
|
lines.push('', 'Pending:');
|
|
360
480
|
for (const item of summary.pending.slice(0, 10)) {
|
package/src/project-state.js
CHANGED
|
@@ -254,7 +254,7 @@ function legacyProcedureSignals(name) {
|
|
|
254
254
|
const common = [/^# .+ Procedure/m];
|
|
255
255
|
const byName = {
|
|
256
256
|
'ingest.md': [/^# Ingest Procedure/m, /wiki\/memory\.md/, /raw\/inbox|raw\/sources/],
|
|
257
|
-
'query.md': [/^# Query Procedure/m, /llm-wiki context/, /Save reusable answers|reusable answers/],
|
|
257
|
+
'query.md': [/^# Query Procedure/m, /llm-wiki context/, /Save reusable answers|reusable answers|Merge reusable facts|reusable facts/],
|
|
258
258
|
'lint.md': [/^# Lint Procedure/m, /llm-wiki lint/, /stale pages|orphan pages/],
|
|
259
259
|
'security.md': [/^# Security Procedure/m, /Redact authentication values|token|private keys/, /raw transcript capture/],
|
|
260
260
|
};
|
|
@@ -359,12 +359,19 @@ function hasKitManagedSignal(text, descriptor) {
|
|
|
359
359
|
return false;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
function hasPatchableGeneratedSignal(text, descriptor) {
|
|
363
|
+
if (!hasLegacyGeneratedSignal(text, descriptor)) return false;
|
|
364
|
+
if (String(descriptor.id || '').startsWith('procedure-') && /^##\s+/m.test(text)) return false;
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
362
368
|
function canPatchDescriptor(state, descriptor, currentText, fileExists = true) {
|
|
363
369
|
if (descriptor.mode === 'create-only') return !fileExists;
|
|
364
370
|
if (descriptor.mode === 'marker') return replaceMarkedBlock(currentText, descriptor.content) !== null;
|
|
365
371
|
return isRecordedManaged(state, descriptor, currentText) ||
|
|
366
372
|
isKnownGeneratedContent(currentText, descriptor) ||
|
|
367
|
-
isLegacyGeneratedContent(currentText, descriptor)
|
|
373
|
+
isLegacyGeneratedContent(currentText, descriptor) ||
|
|
374
|
+
hasPatchableGeneratedSignal(currentText, descriptor);
|
|
368
375
|
}
|
|
369
376
|
|
|
370
377
|
function desiredTextForDescriptor(descriptor, currentText, fileExists = true) {
|
package/src/project.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
writeTextIfMissing,
|
|
10
10
|
} from './fs-utils.js';
|
|
11
11
|
import { LLM_WIKI_DIRS } from './constants.js';
|
|
12
|
+
import { evidenceRefsFromEntry, frontmatterEvidenceRefs } from './evidence.js';
|
|
12
13
|
import { formatMaintenanceContext, maintenanceSummary } from './maintenance.js';
|
|
13
14
|
import { normalizeForStorage, redactText, summarizeForStorage } from './redaction.js';
|
|
14
15
|
import { gitignore, indexPage, llmWikiAgents, logPage, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
|
|
@@ -107,7 +108,8 @@ export async function writeQueryPage(projectRoot, entry) {
|
|
|
107
108
|
const slug = slugify(entry.question, 'query');
|
|
108
109
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'queries', `${day}-${slug}.md`);
|
|
109
110
|
if (await exists(path)) return path;
|
|
110
|
-
const
|
|
111
|
+
const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
|
|
112
|
+
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## Question\n${entry.question}\n\n## Answer Summary\n${entry.result || '(not captured)'}\n\n## Work Notes\n${entry.work || '(not captured)'}\n\n## Verification\n${entry.verification || '(not captured)'}\n\n## Related Pages\n- [[index]]\n\n## Change Log\n- ${day}: Captured automatically by llm-wiki-kit hook.\n`;
|
|
111
113
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
112
114
|
return path;
|
|
113
115
|
}
|
|
@@ -120,7 +122,8 @@ export async function writeDecisionPage(projectRoot, entry) {
|
|
|
120
122
|
const slug = slugify(entry.topic || entry.question || 'decision', 'decision');
|
|
121
123
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'decisions', `${day}-${slug}.md`);
|
|
122
124
|
if (await exists(path)) return path;
|
|
123
|
-
const
|
|
125
|
+
const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
|
|
126
|
+
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 || 'Decision'}\n\n## Decision\n${entry.result || '(captured from assistant response; review needed)'}\n\n## Context\n${entry.question || '(not captured)'}\n\n## Evidence\n${entry.work || '(not captured)'}\n\n## Verification\n${entry.verification || '(not captured)'}\n\n## Open Questions\n${entry.followUp || '(none captured)'}\n\n## Change Log\n- ${day}: Captured automatically by llm-wiki-kit hook.\n`;
|
|
124
127
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
125
128
|
return path;
|
|
126
129
|
}
|
package/src/templates.js
CHANGED
|
@@ -16,12 +16,21 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
|
|
|
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
|
|
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\`.
|
|
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
|
-
- 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 or clearly important.
|
|
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.
|
|
23
23
|
- Never store credentials, tokens, passwords, private keys, or raw \`.env\` contents. Store only redacted summaries when needed.
|
|
24
24
|
|
|
25
|
+
### llm-wiki-kit Implementation Plans
|
|
26
|
+
|
|
27
|
+
- When implementing an approved llm-wiki-kit plan, read the complete plan first and treat it as the implementation source of truth.
|
|
28
|
+
- A valid plan must include history/context, lifecycle intent, exact source paths, test strategy, release verification, and commit/push requirements.
|
|
29
|
+
- Do not implement feature lists in isolation. Each feature must be integrated into the hook-first, answer-first, local Markdown knowledge lifecycle.
|
|
30
|
+
- \`llms.txt\`/exports are not passive artifacts; they are agent onboarding, handoff, retrieval-eval, and external-consumption manifests.
|
|
31
|
+
- \`evidence_refs\`, maintenance review, context ranking explanations, export, and eval must share the same durable wiki visibility policy.
|
|
32
|
+
- Never store npm tokens, WinRM credentials, private keys, raw \`.env\`, or full raw transcripts in wiki, logs, generated exports, or commits.
|
|
33
|
+
|
|
25
34
|
<!-- llm-wiki-kit:end -->
|
|
26
35
|
`;
|
|
27
36
|
}
|
|
@@ -55,13 +64,23 @@ These rules replace older OMX/OMC/\`omx_wiki/\` rules for this project.
|
|
|
55
64
|
- Important claims should include at least one of: \`source_ids\`, file paths, or verification commands.
|
|
56
65
|
- When durable knowledge appears, search existing \`wiki/\` pages before creating a new page.
|
|
57
66
|
- Do not promote simple answers, status checks, keyword-only replies, or one-off chat into live Q&A, \`wiki/queries\`, or maintenance.
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
67
|
+
- 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
|
+
- 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 editing. Show only short reminders at \`SessionStart\`/\`InstructionsLoaded\`, and compact prompt-time reminders when durable candidates or review thresholds need agent attention.
|
|
61
71
|
- Keep \`wiki/memory.md\` short. Use links to current state and important documents instead of long explanations.
|
|
62
72
|
- Preserve contradictions in \`Contradictions\` or \`Open Questions\`; do not overwrite them silently.
|
|
63
73
|
- Do not store credentials, tokens, passwords, private keys, or raw \`.env\` contents in wiki.
|
|
64
74
|
|
|
75
|
+
### llm-wiki-kit Implementation Plans
|
|
76
|
+
|
|
77
|
+
- When implementing an approved llm-wiki-kit plan, read the complete plan first and treat it as the implementation source of truth.
|
|
78
|
+
- A valid plan must include history/context, lifecycle intent, exact source paths, test strategy, release verification, and commit/push requirements.
|
|
79
|
+
- Do not implement feature lists in isolation. Each feature must be integrated into the hook-first, answer-first, local Markdown knowledge lifecycle.
|
|
80
|
+
- \`llms.txt\`/exports are not passive artifacts; they are agent onboarding, handoff, retrieval-eval, and external-consumption manifests.
|
|
81
|
+
- \`evidence_refs\`, maintenance review, context ranking explanations, export, and eval must share the same durable wiki visibility policy.
|
|
82
|
+
- Never store npm tokens, WinRM credentials, private keys, raw \`.env\`, or full raw transcripts in wiki, logs, generated exports, or commits.
|
|
83
|
+
|
|
65
84
|
## Page Format
|
|
66
85
|
Use YAML frontmatter when creating wiki pages:
|
|
67
86
|
|
|
@@ -70,6 +89,7 @@ Use YAML frontmatter when creating wiki pages:
|
|
|
70
89
|
title: ""
|
|
71
90
|
type: "source | concept | entity | decision | architecture | debugging | context | query | session-log | convention"
|
|
72
91
|
source_ids: []
|
|
92
|
+
evidence_refs: []
|
|
73
93
|
status: "draft | reviewed | stale | archived"
|
|
74
94
|
last_updated: "YYYY-MM-DD"
|
|
75
95
|
confidence: "high | medium | low"
|
|
@@ -86,7 +106,7 @@ superseded_by: []
|
|
|
86
106
|
- query: use hook-injected context when useful, but answer the current request first. Manual commands are diagnostics, not required daily workflow.
|
|
87
107
|
- lint: agent helper for user-requested or wiki maintenance work. Do not run it every turn.
|
|
88
108
|
- consolidate: safely refresh generated blocks in \`memory.md\`/\`index.md\`. Do not overwrite handwritten sections or curated document bodies.
|
|
89
|
-
- maintenance: \`outputs/maintenance/queue.md\` stores stop/start cleanup candidates. Review
|
|
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\`.
|
|
90
110
|
`;
|
|
91
111
|
}
|
|
92
112
|
|
|
@@ -131,6 +151,7 @@ export function memoryPage() {
|
|
|
131
151
|
title: "LLM Wiki Memory"
|
|
132
152
|
type: "context"
|
|
133
153
|
source_ids: []
|
|
154
|
+
evidence_refs: []
|
|
134
155
|
status: "draft"
|
|
135
156
|
last_updated: "unknown"
|
|
136
157
|
confidence: "medium"
|
|
@@ -178,7 +199,7 @@ export function procedure(name) {
|
|
|
178
199
|
3. Create or update \`wiki/sources/<slug>.md\` only when a source summary is useful.
|
|
179
200
|
4. Before creating a new page, look for related concept/entity/decision/architecture/debugging/context pages and update those first.
|
|
180
201
|
5. Do not create duplicates. Merge facts into existing pages when they already exist.
|
|
181
|
-
6. Do not promote simple answers or one-off chat into durable wiki.
|
|
202
|
+
6. Do not promote simple answers or one-off chat into durable wiki, but do review hook-suggested durable candidates for reusable facts.
|
|
182
203
|
7. Important claims should record source references, confidence, memory type, importance, and verification status.
|
|
183
204
|
8. When durable entry points change, update \`wiki/memory.md\` briefly or use \`llm-wiki consolidate\`.
|
|
184
205
|
9. Add a short entry to \`wiki/log.md\` for meaningful wiki changes.
|
|
@@ -191,8 +212,8 @@ export function procedure(name) {
|
|
|
191
212
|
4. Inspect raw sources only when exact evidence matters.
|
|
192
213
|
5. Separate verified facts from inference.
|
|
193
214
|
6. Use \`llm-wiki context "<query>"\` only when manual inspection is useful. Add \`--include-episodic\` only for historical query/context pages, and \`--include-archived\` only for archived/superseded pages.
|
|
194
|
-
7. Do not promote one-off answers, status checks, or keyword-only replies to durable wiki or live Q&A. Archive only work turns with tool evidence, changed-file evidence, verification,
|
|
195
|
-
8. Merge reusable facts into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` based on importance and user consent flow.
|
|
215
|
+
7. Do not promote one-off answers, status checks, or keyword-only replies to durable wiki or live Q&A. Archive only work turns with tool evidence, changed-file evidence, verification, structured decision/debugging conclusions, or hook-suggested durable candidates.
|
|
216
|
+
8. Merge reusable facts into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` based on importance, hook suggestions, and user consent flow.
|
|
196
217
|
`,
|
|
197
218
|
'lint.md': `# Lint Procedure
|
|
198
219
|
|
|
@@ -204,7 +225,7 @@ Periodic maintenance is an agent-side task, not a per-turn user command. When ne
|
|
|
204
225
|
|
|
205
226
|
1. \`llm-wiki lint --workspace <project>\`
|
|
206
227
|
2. \`llm-wiki maintenance --workspace <project>\`
|
|
207
|
-
3. Merge pending
|
|
228
|
+
3. Merge pending or hook-suggested durable candidates into existing durable pages when appropriate, then mark them \`done\` or \`skipped\`.
|
|
208
229
|
4. \`llm-wiki consolidate --workspace <project> --dry-run\`
|
|
209
230
|
5. Run \`llm-wiki consolidate --workspace <project>\` when the dry run is acceptable.
|
|
210
231
|
|