llm-wiki-kit 0.1.5 → 0.1.7
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 +39 -16
- package/docs/concepts.md +16 -3
- package/docs/integrations/claude-code.md +2 -0
- package/docs/integrations/codex.md +2 -2
- package/docs/operations.md +101 -5
- package/docs/research/baseline.md +5 -1
- package/docs/research/future-large-scale.md +27 -0
- package/docs/security.md +2 -0
- package/docs/troubleshooting.md +30 -1
- package/package.json +4 -1
- package/src/cli.js +52 -3
- package/src/consolidate.js +203 -0
- package/src/hook.js +9 -3
- package/src/install.js +4 -2
- package/src/project-state.js +335 -14
- package/src/project.js +16 -57
- package/src/state.js +5 -0
- package/src/templates.js +194 -9
- package/src/update.js +34 -5
- package/src/wiki-lint.js +281 -0
- package/src/wiki-model.js +263 -0
- package/src/wiki-search.js +246 -0
package/src/project-state.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join, relative } from 'path';
|
|
2
2
|
import { backupFile, ensureDir, exists, readJson, readText, sha256, writeJson, writeText } from './fs-utils.js';
|
|
3
3
|
import { runtimeVersion } from './version.js';
|
|
4
|
-
import { llmWikiAgents, procedure, rootAgentsPolicy } from './templates.js';
|
|
4
|
+
import { llmWikiAgents, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
|
|
5
5
|
|
|
6
6
|
export const PROJECT_STATE_SCHEMA_VERSION = 1;
|
|
7
7
|
export const PROJECT_STATE_FILE = '.kit-state.json';
|
|
@@ -27,17 +27,199 @@ function templateDescriptors() {
|
|
|
27
27
|
path: join('llm-wiki', 'AGENTS.md'),
|
|
28
28
|
content: llmWikiAgents(),
|
|
29
29
|
mode: 'generated-file',
|
|
30
|
-
|
|
30
|
+
legacyContents: legacyLlmWikiAgentsContents(),
|
|
31
|
+
legacySignals: [
|
|
32
|
+
/^# LLM Wiki Agent Rules/m,
|
|
33
|
+
/Generated by llm-wiki-kit/m,
|
|
34
|
+
/## Purpose/m,
|
|
35
|
+
/## Core Rules/m,
|
|
36
|
+
/## Operations/m,
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'wiki-memory',
|
|
41
|
+
path: join('llm-wiki', 'wiki', 'memory.md'),
|
|
42
|
+
content: memoryPage(),
|
|
43
|
+
mode: 'create-only',
|
|
31
44
|
},
|
|
32
45
|
...PROCEDURE_NAMES.map((name) => ({
|
|
33
46
|
id: `procedure-${name.replace(/\.md$/, '')}`,
|
|
34
47
|
path: join('llm-wiki', 'procedures', name),
|
|
35
48
|
content: procedure(name),
|
|
36
49
|
mode: 'generated-file',
|
|
50
|
+
legacyContents: legacyProcedureContents(name),
|
|
51
|
+
legacySignals: legacyProcedureSignals(name),
|
|
37
52
|
})),
|
|
38
53
|
];
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
function legacyLlmWikiAgentsContents() {
|
|
57
|
+
return [
|
|
58
|
+
`# LLM Wiki Agent Rules
|
|
59
|
+
|
|
60
|
+
Generated by llm-wiki-kit <version>.
|
|
61
|
+
|
|
62
|
+
## Purpose
|
|
63
|
+
Maintain a living Markdown LLM Wiki from immutable source files and redacted Codex/Claude Code session events.
|
|
64
|
+
These rules supersede older OMX/OMC/\`omx_wiki/\` LLM Wiki rules for this project.
|
|
65
|
+
|
|
66
|
+
## Directories
|
|
67
|
+
- \`raw/\`: immutable or redacted source material. Never edit original source captures.
|
|
68
|
+
- \`wiki/\`: AI-maintained knowledge pages.
|
|
69
|
+
- \`outputs/\`: live Q&A summaries, reports, and generated briefs.
|
|
70
|
+
- \`procedures/\`: detailed operating rules for ingest, query, lint, and security.
|
|
71
|
+
|
|
72
|
+
## Core Rules
|
|
73
|
+
- Never modify \`raw/\` source material except to append redacted session envelopes generated by hooks.
|
|
74
|
+
- Do not state unsupported claims as facts.
|
|
75
|
+
- Mark inference explicitly.
|
|
76
|
+
- Add \`source_ids\` or file references for important claims.
|
|
77
|
+
- Update \`wiki/index.md\` and \`wiki/log.md\` whenever new durable knowledge is created.
|
|
78
|
+
- Preserve contradictions in \`Contradictions\` or \`Open Questions\` instead of overwriting them.
|
|
79
|
+
- Preserve useful work context and redact authentication values before writing durable notes.
|
|
80
|
+
|
|
81
|
+
## Page Format
|
|
82
|
+
Use YAML frontmatter when creating wiki pages:
|
|
83
|
+
|
|
84
|
+
\`\`\`yaml
|
|
85
|
+
---
|
|
86
|
+
title: ""
|
|
87
|
+
type: "source | concept | entity | decision | architecture | debugging | context | query | session-log | convention"
|
|
88
|
+
source_ids: []
|
|
89
|
+
status: "draft | reviewed | stale"
|
|
90
|
+
last_updated: "YYYY-MM-DD"
|
|
91
|
+
confidence: "high | medium | low"
|
|
92
|
+
---
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
## Operations
|
|
96
|
+
- ingest: read new raw files, create or update wiki pages, then update \`wiki/index.md\` and \`wiki/log.md\`.
|
|
97
|
+
- query: start from \`wiki/index.md\`, read relevant wiki pages, answer with source references, and save reusable answers.
|
|
98
|
+
- lint: find stale pages, orphan pages, broken wiki links, missing sources, duplicate concepts, contradictions, and missing links.`,
|
|
99
|
+
`# LLM Wiki Agent Rules
|
|
100
|
+
|
|
101
|
+
Generated by llm-wiki-kit <version>.
|
|
102
|
+
|
|
103
|
+
## Purpose
|
|
104
|
+
Maintain a living Markdown LLM Wiki from immutable source files and redacted Codex/Claude Code session events.
|
|
105
|
+
These rules supersede older OMX/OMC/\`omx_wiki/\` LLM Wiki rules for this project.
|
|
106
|
+
|
|
107
|
+
## Directories
|
|
108
|
+
- \`raw/\`: immutable or redacted source material. Never edit original source captures.
|
|
109
|
+
- \`wiki/\`: AI-maintained knowledge pages. \`wiki/memory.md\` is the short hot index injected into hook context.
|
|
110
|
+
- \`outputs/\`: live Q&A summaries, reports, and generated briefs.
|
|
111
|
+
- \`procedures/\`: detailed operating rules for ingest, query, lint, and security.
|
|
112
|
+
|
|
113
|
+
## Core Rules
|
|
114
|
+
- Never modify \`raw/\` source material except to append redacted session envelopes generated by hooks.
|
|
115
|
+
- Do not state unsupported claims as facts.
|
|
116
|
+
- Mark inference explicitly.
|
|
117
|
+
- Add \`source_ids\` or file references for important claims.
|
|
118
|
+
- Update \`wiki/memory.md\`, \`wiki/index.md\`, and \`wiki/log.md\` whenever new durable knowledge changes the active project map.
|
|
119
|
+
- Preserve contradictions in \`Contradictions\` or \`Open Questions\` instead of overwriting them.
|
|
120
|
+
- Preserve useful work context and redact authentication values before writing durable notes.
|
|
121
|
+
|
|
122
|
+
## Page Format
|
|
123
|
+
Use YAML frontmatter when creating wiki pages:
|
|
124
|
+
|
|
125
|
+
\`\`\`yaml
|
|
126
|
+
---
|
|
127
|
+
title: ""
|
|
128
|
+
type: "source | concept | entity | decision | architecture | debugging | context | query | session-log | convention"
|
|
129
|
+
source_ids: []
|
|
130
|
+
status: "draft | reviewed | stale"
|
|
131
|
+
last_updated: "YYYY-MM-DD"
|
|
132
|
+
confidence: "high | medium | low"
|
|
133
|
+
memory_type: "semantic | episodic | procedural"
|
|
134
|
+
importance: 1
|
|
135
|
+
last_verified: "YYYY-MM-DD | unknown"
|
|
136
|
+
supersedes: []
|
|
137
|
+
superseded_by: []
|
|
138
|
+
---
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
## Operations
|
|
142
|
+
- ingest: read \`wiki/memory.md\` and \`wiki/index.md\`, ingest new raw files, create or update wiki pages, then update the active map and \`wiki/log.md\`.
|
|
143
|
+
- query: start from \`llm-wiki context "<query>"\` or \`wiki/memory.md\`, read relevant wiki pages, answer with source references, and save reusable answers.
|
|
144
|
+
- lint: run \`llm-wiki lint --workspace <project>\` to find stale pages, orphan pages, broken wiki/Markdown links, unsafe source IDs, secret-like content, missing sources, duplicate concepts, contradictions, and missing links.
|
|
145
|
+
- consolidate: run \`llm-wiki consolidate --workspace <project>\` after meaningful wiki growth to refresh generated memory/index blocks without overwriting hand-written notes. Default query/context/session-log pages are excluded unless explicitly durable.`,
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function legacyProcedureContents(name) {
|
|
150
|
+
const early = {
|
|
151
|
+
'ingest.md': `# Ingest Procedure
|
|
152
|
+
|
|
153
|
+
1. Read \`wiki/index.md\` first.
|
|
154
|
+
2. Inspect new material under \`raw/inbox/\` or \`raw/sources/\`.
|
|
155
|
+
3. Create or update \`wiki/sources/<slug>.md\` for each source.
|
|
156
|
+
4. Update related concept, entity, decision, architecture, debugging, or context pages.
|
|
157
|
+
5. Prefer updating existing pages over creating duplicates.
|
|
158
|
+
6. Add source references and confidence.
|
|
159
|
+
7. Update \`wiki/index.md\` and append to \`wiki/log.md\`.`,
|
|
160
|
+
'query.md': `# Query Procedure
|
|
161
|
+
|
|
162
|
+
1. Start from \`wiki/index.md\`.
|
|
163
|
+
2. Search \`wiki/\` for relevant pages.
|
|
164
|
+
3. Read the smallest useful set of pages first.
|
|
165
|
+
4. Use raw sources only when exact evidence matters.
|
|
166
|
+
5. Separate verified facts from inference.
|
|
167
|
+
6. Save reusable answers into \`wiki/queries/\` and link them from related pages.`,
|
|
168
|
+
'lint.md': `# Lint Procedure
|
|
169
|
+
|
|
170
|
+
Check for stale pages, orphan pages, broken wiki links, missing sources, duplicate concepts, unsupported claims, and unresolved contradictions. Prefer producing a review report before automatic edits.`,
|
|
171
|
+
'security.md': `# Security Procedure
|
|
172
|
+
|
|
173
|
+
- Never store credentials, tokens, private keys, \`.env\` contents, customer identifiers, or private personal data.
|
|
174
|
+
- Redact before writing hook payloads or summaries.
|
|
175
|
+
- Full raw transcript capture is disabled by default and must be explicitly enabled by project policy.
|
|
176
|
+
- If a file or prompt looks secret-bearing, do not persist it and ask for confirmation before reading further.`,
|
|
177
|
+
};
|
|
178
|
+
const layered = {
|
|
179
|
+
'ingest.md': `# Ingest Procedure
|
|
180
|
+
|
|
181
|
+
1. Read \`wiki/memory.md\` and \`wiki/index.md\` first.
|
|
182
|
+
2. Inspect new material under \`raw/inbox/\` or \`raw/sources/\`.
|
|
183
|
+
3. Create or update \`wiki/sources/<slug>.md\` for each source.
|
|
184
|
+
4. Update related concept, entity, decision, architecture, debugging, or context pages.
|
|
185
|
+
5. Prefer updating existing pages over creating duplicates.
|
|
186
|
+
6. Add source references, confidence, memory type, importance, and verification status.
|
|
187
|
+
7. Update \`wiki/memory.md\` or run \`llm-wiki consolidate\` when durable entry points change.
|
|
188
|
+
8. Update \`wiki/index.md\` and append to \`wiki/log.md\`.`,
|
|
189
|
+
'query.md': `# Query Procedure
|
|
190
|
+
|
|
191
|
+
1. Start from \`llm-wiki context "<query>"\` or read \`wiki/memory.md\` and \`wiki/index.md\`.
|
|
192
|
+
2. Use \`--limit\` or \`--no-expand\` when you need a narrower context pack.
|
|
193
|
+
3. Search \`wiki/\` for relevant pages.
|
|
194
|
+
4. Read the smallest useful set of pages first.
|
|
195
|
+
5. Use raw sources only when exact evidence matters.
|
|
196
|
+
6. Separate verified facts from inference.
|
|
197
|
+
7. Save reusable answers into \`wiki/queries/\` and link them from related pages.`,
|
|
198
|
+
'lint.md': `# Lint Procedure
|
|
199
|
+
|
|
200
|
+
Run \`llm-wiki lint --workspace <project>\` to check for stale pages, orphan pages, broken wiki/Markdown links, unsafe source IDs, secret-like content, missing sources, duplicate concepts, unsupported claims, and unresolved contradictions. Prefer producing a review report before automatic edits.`,
|
|
201
|
+
'security.md': `# Security Procedure
|
|
202
|
+
|
|
203
|
+
- Preserve useful work context for the local project wiki.
|
|
204
|
+
- Do not block reads or tool calls only because they look sensitive.
|
|
205
|
+
- Redact authentication values such as tokens, passwords, bearer credentials, and private keys before writing hook payloads, summaries, or context packs.
|
|
206
|
+
- Run \`llm-wiki lint --workspace <project>\` when sensitive material might have entered wiki pages; secret-like content is reported as an error.
|
|
207
|
+
- Full raw transcript capture is disabled by default and must be explicitly enabled by project policy.`,
|
|
208
|
+
};
|
|
209
|
+
return [early[name], layered[name]].filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function legacyProcedureSignals(name) {
|
|
213
|
+
const common = [/^# .+ Procedure/m];
|
|
214
|
+
const byName = {
|
|
215
|
+
'ingest.md': [/^# Ingest Procedure/m, /wiki\/memory\.md/, /raw\/inbox|raw\/sources/],
|
|
216
|
+
'query.md': [/^# Query Procedure/m, /llm-wiki context/, /Save reusable answers|reusable answers/],
|
|
217
|
+
'lint.md': [/^# Lint Procedure/m, /llm-wiki lint/, /stale pages|orphan pages/],
|
|
218
|
+
'security.md': [/^# Security Procedure/m, /Redact authentication values|token|private keys/, /raw transcript capture/],
|
|
219
|
+
};
|
|
220
|
+
return byName[name] || common;
|
|
221
|
+
}
|
|
222
|
+
|
|
41
223
|
function emptyState() {
|
|
42
224
|
return {
|
|
43
225
|
schemaVersion: PROJECT_STATE_SCHEMA_VERSION,
|
|
@@ -74,39 +256,91 @@ export async function writeProjectState(projectRoot, state) {
|
|
|
74
256
|
}
|
|
75
257
|
|
|
76
258
|
function replaceMarkedBlock(current, replacement) {
|
|
259
|
+
if (markerStatus(current) !== 'complete') return null;
|
|
77
260
|
const start = current.indexOf(ROOT_POLICY_START);
|
|
78
261
|
const end = current.indexOf(ROOT_POLICY_END);
|
|
79
|
-
if (start === -1 || end === -1 || end < start) return null;
|
|
80
262
|
const afterEnd = end + ROOT_POLICY_END.length;
|
|
81
263
|
return `${current.slice(0, start)}${replacement.trimStart().trimEnd()}${current.slice(afterEnd)}`;
|
|
82
264
|
}
|
|
83
265
|
|
|
84
|
-
function
|
|
85
|
-
return
|
|
266
|
+
function markerCount(current, marker) {
|
|
267
|
+
return current.split(marker).length - 1;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function markerStatus(current) {
|
|
271
|
+
const start = current.indexOf(ROOT_POLICY_START);
|
|
272
|
+
const end = current.indexOf(ROOT_POLICY_END);
|
|
273
|
+
const starts = markerCount(current, ROOT_POLICY_START);
|
|
274
|
+
const ends = markerCount(current, ROOT_POLICY_END);
|
|
275
|
+
if (starts === 0 && ends === 0) return 'missing';
|
|
276
|
+
if (starts !== 1 || ends !== 1 || end < start) return 'malformed';
|
|
277
|
+
return 'complete';
|
|
86
278
|
}
|
|
87
279
|
|
|
88
280
|
function isRecordedManaged(state, descriptor, currentText) {
|
|
89
281
|
const recorded = state.managedTemplates?.[descriptor.id];
|
|
90
282
|
if (!recorded) return false;
|
|
283
|
+
if (descriptor.mode === 'create-only') return true;
|
|
91
284
|
if (descriptor.mode === 'marker') return true;
|
|
92
|
-
return
|
|
285
|
+
return Boolean(recorded.hash) && recorded.hash === templateHash(currentText);
|
|
93
286
|
}
|
|
94
287
|
|
|
95
288
|
function isKnownGeneratedContent(text, descriptor) {
|
|
289
|
+
if (descriptor.mode === 'create-only') return false;
|
|
96
290
|
if (descriptor.mode === 'marker') return replaceMarkedBlock(text, descriptor.content) !== null;
|
|
97
|
-
return templateHash(text) === templateHash(descriptor.content)
|
|
291
|
+
return templateHash(text) === templateHash(descriptor.content);
|
|
98
292
|
}
|
|
99
293
|
|
|
100
|
-
function
|
|
294
|
+
function normalizeGeneratedText(text) {
|
|
295
|
+
return String(text || '')
|
|
296
|
+
.replace(/\r\n/g, '\n')
|
|
297
|
+
.replace(/Generated by llm-wiki-kit [^\n]+/g, 'Generated by llm-wiki-kit <version>.')
|
|
298
|
+
.trim();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isLegacyGeneratedContent(text, descriptor) {
|
|
302
|
+
if (descriptor.mode !== 'generated-file') return false;
|
|
303
|
+
if (!Array.isArray(descriptor.legacyContents) || descriptor.legacyContents.length === 0) return false;
|
|
304
|
+
const normalized = normalizeGeneratedText(text);
|
|
305
|
+
return descriptor.legacyContents.some((content) => normalizeGeneratedText(content) === normalized);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasLegacyGeneratedSignal(text, descriptor) {
|
|
309
|
+
if (!Array.isArray(descriptor.legacySignals) || descriptor.legacySignals.length === 0) return false;
|
|
310
|
+
return descriptor.legacySignals.every((pattern) => pattern.test(text));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function hasKitManagedSignal(text, descriptor) {
|
|
314
|
+
if (descriptor.mode === 'marker') return markerStatus(text) !== 'missing';
|
|
315
|
+
if (descriptor.mode === 'generated-file') {
|
|
316
|
+
return isLegacyGeneratedContent(text, descriptor) || hasLegacyGeneratedSignal(text, descriptor) || /Generated by llm-wiki-kit/.test(text);
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function canPatchDescriptor(state, descriptor, currentText, fileExists = true) {
|
|
322
|
+
if (descriptor.mode === 'create-only') return !fileExists;
|
|
101
323
|
if (descriptor.mode === 'marker') return replaceMarkedBlock(currentText, descriptor.content) !== null;
|
|
102
|
-
return isRecordedManaged(state, descriptor, currentText) ||
|
|
324
|
+
return isRecordedManaged(state, descriptor, currentText) ||
|
|
325
|
+
isKnownGeneratedContent(currentText, descriptor) ||
|
|
326
|
+
isLegacyGeneratedContent(currentText, descriptor);
|
|
103
327
|
}
|
|
104
328
|
|
|
105
|
-
function desiredTextForDescriptor(descriptor, currentText) {
|
|
329
|
+
function desiredTextForDescriptor(descriptor, currentText, fileExists = true) {
|
|
330
|
+
if (descriptor.mode === 'create-only') return fileExists ? currentText : descriptor.content;
|
|
106
331
|
if (descriptor.mode === 'marker') return replaceMarkedBlock(currentText, descriptor.content);
|
|
107
332
|
return descriptor.content;
|
|
108
333
|
}
|
|
109
334
|
|
|
335
|
+
async function createOnlyParentExists(projectRoot, descriptor) {
|
|
336
|
+
if (descriptor.mode !== 'create-only') return true;
|
|
337
|
+
return exists(dirname(join(projectRoot, descriptor.path)));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function descriptorParentExists(projectRoot, descriptor) {
|
|
341
|
+
return exists(dirname(join(projectRoot, descriptor.path)));
|
|
342
|
+
}
|
|
343
|
+
|
|
110
344
|
export async function inspectProjectState(projectRoot) {
|
|
111
345
|
const state = await readProjectState(projectRoot);
|
|
112
346
|
const runtime = runtimeVersion();
|
|
@@ -116,12 +350,24 @@ export async function inspectProjectState(projectRoot) {
|
|
|
116
350
|
const absolutePath = join(projectRoot, descriptor.path);
|
|
117
351
|
const fileExists = await exists(absolutePath);
|
|
118
352
|
const currentText = fileExists ? await readText(absolutePath) : '';
|
|
119
|
-
const desiredText =
|
|
353
|
+
const desiredText = desiredTextForDescriptor(descriptor, currentText, fileExists);
|
|
120
354
|
const desiredHash = templateHash(desiredText || descriptor.content);
|
|
121
355
|
const currentHash = fileExists ? templateHash(currentText) : null;
|
|
122
356
|
const recorded = state.managedTemplates[descriptor.id] || null;
|
|
123
|
-
const
|
|
124
|
-
const
|
|
357
|
+
const markerState = descriptor.mode === 'marker' && fileExists ? markerStatus(currentText) : null;
|
|
358
|
+
const parentExists = await descriptorParentExists(projectRoot, descriptor);
|
|
359
|
+
const patchable = descriptor.mode === 'create-only' && !fileExists && !parentExists
|
|
360
|
+
? false
|
|
361
|
+
: (!fileExists && descriptor.mode === 'generated-file'
|
|
362
|
+
? parentExists
|
|
363
|
+
: (markerState === 'malformed' ? false : canPatchDescriptor(state, descriptor, currentText, fileExists)));
|
|
364
|
+
const needsAttention = (fileExists || recorded) &&
|
|
365
|
+
!patchable &&
|
|
366
|
+
currentText !== (desiredText || '') &&
|
|
367
|
+
(fileExists ? hasKitManagedSignal(currentText, descriptor) : descriptor.mode !== 'create-only');
|
|
368
|
+
const current = descriptor.mode === 'create-only'
|
|
369
|
+
? fileExists
|
|
370
|
+
: fileExists && desiredText !== null && currentHash === templateHash(desiredText);
|
|
125
371
|
|
|
126
372
|
files.push({
|
|
127
373
|
id: descriptor.id,
|
|
@@ -129,10 +375,12 @@ export async function inspectProjectState(projectRoot) {
|
|
|
129
375
|
exists: fileExists,
|
|
130
376
|
patchable,
|
|
131
377
|
current,
|
|
378
|
+
needsAttention,
|
|
132
379
|
currentHash,
|
|
133
380
|
desiredHash,
|
|
134
381
|
recordedHash: recorded?.hash || null,
|
|
135
382
|
recordedVersion: recorded?.version || null,
|
|
383
|
+
reason: markerState === 'malformed' ? 'malformed-marker' : (!fileExists ? 'missing' : null),
|
|
136
384
|
});
|
|
137
385
|
}
|
|
138
386
|
|
|
@@ -142,11 +390,31 @@ export async function inspectProjectState(projectRoot) {
|
|
|
142
390
|
schemaVersion: state.schemaVersion,
|
|
143
391
|
lastRuntimeVersionApplied: state.lastRuntimeVersionApplied,
|
|
144
392
|
runtimeUpToDateWithProject: state.lastRuntimeVersionApplied === runtime,
|
|
145
|
-
managedFilesCurrent: files.every((file) => file.current || !file.patchable),
|
|
393
|
+
managedFilesCurrent: files.every((file) => file.current || (!file.patchable && !file.needsAttention)),
|
|
146
394
|
managedFiles: files,
|
|
147
395
|
};
|
|
148
396
|
}
|
|
149
397
|
|
|
398
|
+
export function formatProjectMaintenanceContext(inspection) {
|
|
399
|
+
const files = inspection?.managedFiles || [];
|
|
400
|
+
const attention = files.filter((file) => file.needsAttention);
|
|
401
|
+
const outdated = files.filter((file) => !file.current && file.patchable);
|
|
402
|
+
if (attention.length === 0 && outdated.length === 0) return '';
|
|
403
|
+
|
|
404
|
+
const lines = [
|
|
405
|
+
'LLM Wiki maintenance note:',
|
|
406
|
+
'- 이전 버전의 llm-wiki-kit 규칙/문서가 남아 있을 수 있다.',
|
|
407
|
+
'- 확실히 kit가 관리하는 영역은 자동 갱신된다. 사용자 편집 가능성이 있는 문서는 덮어쓰지 말고 기존 내용을 보존한 채 현재 규칙에 맞게 자연스럽게 정리한다.',
|
|
408
|
+
];
|
|
409
|
+
if (outdated.length > 0) {
|
|
410
|
+
lines.push(`- 자동 갱신 대상: ${outdated.map((file) => file.path).join(', ')}`);
|
|
411
|
+
}
|
|
412
|
+
if (attention.length > 0) {
|
|
413
|
+
lines.push(`- agent 확인 필요: ${attention.map((file) => file.path).join(', ')}`);
|
|
414
|
+
}
|
|
415
|
+
return lines.join('\n');
|
|
416
|
+
}
|
|
417
|
+
|
|
150
418
|
export async function recordManagedTemplates(projectRoot) {
|
|
151
419
|
const state = await readProjectState(projectRoot);
|
|
152
420
|
const runtime = runtimeVersion();
|
|
@@ -155,6 +423,14 @@ export async function recordManagedTemplates(projectRoot) {
|
|
|
155
423
|
const absolutePath = join(projectRoot, descriptor.path);
|
|
156
424
|
if (!(await exists(absolutePath))) continue;
|
|
157
425
|
const currentText = await readText(absolutePath);
|
|
426
|
+
if (descriptor.mode === 'create-only') {
|
|
427
|
+
state.managedTemplates[descriptor.id] = {
|
|
428
|
+
path: descriptor.path,
|
|
429
|
+
version: runtime,
|
|
430
|
+
hash: templateHash(currentText),
|
|
431
|
+
};
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
158
434
|
if (!canPatchDescriptor(state, descriptor, currentText)) continue;
|
|
159
435
|
state.managedTemplates[descriptor.id] = {
|
|
160
436
|
path: descriptor.path,
|
|
@@ -177,11 +453,56 @@ export async function applyProjectTemplateUpdate(projectRoot, options = {}) {
|
|
|
177
453
|
for (const descriptor of templateDescriptors()) {
|
|
178
454
|
const absolutePath = join(projectRoot, descriptor.path);
|
|
179
455
|
if (!(await exists(absolutePath))) {
|
|
456
|
+
if (descriptor.mode === 'create-only') {
|
|
457
|
+
if (!(await descriptorParentExists(projectRoot, descriptor))) {
|
|
458
|
+
skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'missing-wiki' });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (!options.dryRun) {
|
|
462
|
+
await ensureDir(dirname(absolutePath));
|
|
463
|
+
await writeText(absolutePath, descriptor.content, 'utf8');
|
|
464
|
+
}
|
|
465
|
+
changed.push({ id: descriptor.id, path: descriptor.path });
|
|
466
|
+
state.managedTemplates[descriptor.id] = {
|
|
467
|
+
path: descriptor.path,
|
|
468
|
+
version: runtime,
|
|
469
|
+
hash: templateHash(descriptor.content),
|
|
470
|
+
};
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (descriptor.mode === 'generated-file' && await descriptorParentExists(projectRoot, descriptor)) {
|
|
474
|
+
if (!options.dryRun) {
|
|
475
|
+
await ensureDir(dirname(absolutePath));
|
|
476
|
+
await writeText(absolutePath, descriptor.content, 'utf8');
|
|
477
|
+
}
|
|
478
|
+
changed.push({ id: descriptor.id, path: descriptor.path });
|
|
479
|
+
state.managedTemplates[descriptor.id] = {
|
|
480
|
+
path: descriptor.path,
|
|
481
|
+
version: runtime,
|
|
482
|
+
hash: templateHash(descriptor.content),
|
|
483
|
+
};
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
180
486
|
skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'missing' });
|
|
181
487
|
continue;
|
|
182
488
|
}
|
|
183
489
|
|
|
184
490
|
const currentText = await readText(absolutePath);
|
|
491
|
+
if (descriptor.mode === 'marker' && markerStatus(currentText) === 'malformed') {
|
|
492
|
+
skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'malformed-marker' });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (descriptor.mode === 'create-only') {
|
|
497
|
+
unchanged.push({ id: descriptor.id, path: descriptor.path });
|
|
498
|
+
state.managedTemplates[descriptor.id] = {
|
|
499
|
+
path: descriptor.path,
|
|
500
|
+
version: runtime,
|
|
501
|
+
hash: templateHash(currentText),
|
|
502
|
+
};
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
185
506
|
if (!canPatchDescriptor(state, descriptor, currentText)) {
|
|
186
507
|
skipped.push({ id: descriptor.id, path: descriptor.path, reason: 'not-managed' });
|
|
187
508
|
continue;
|
package/src/project.js
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
appendText,
|
|
4
4
|
ensureDir,
|
|
5
5
|
exists,
|
|
6
|
-
listMarkdownFiles,
|
|
7
6
|
readText,
|
|
8
7
|
timeKst,
|
|
9
8
|
todayKst,
|
|
@@ -11,8 +10,9 @@ import {
|
|
|
11
10
|
} from './fs-utils.js';
|
|
12
11
|
import { LLM_WIKI_DIRS } from './constants.js';
|
|
13
12
|
import { normalizeForStorage, redactText, summarizeForStorage } from './redaction.js';
|
|
14
|
-
import { gitignore, indexPage, llmWikiAgents, logPage, procedure, rootAgentsPolicy } from './templates.js';
|
|
15
|
-
import { recordManagedTemplates } from './project-state.js';
|
|
13
|
+
import { gitignore, indexPage, llmWikiAgents, logPage, memoryPage, procedure, rootAgentsPolicy } from './templates.js';
|
|
14
|
+
import { formatProjectMaintenanceContext, inspectProjectState, recordManagedTemplates } from './project-state.js';
|
|
15
|
+
import { buildContextPack, formatContextPack, searchWiki as searchWikiWithIndex } from './wiki-search.js';
|
|
16
16
|
|
|
17
17
|
export async function bootstrapProject(projectRoot, options = {}) {
|
|
18
18
|
if (process.env.LLM_WIKI_KIT_DISABLE_BOOTSTRAP === '1') return { created: false };
|
|
@@ -28,6 +28,7 @@ export async function bootstrapProject(projectRoot, options = {}) {
|
|
|
28
28
|
|
|
29
29
|
await maybeCreate(join(base, 'AGENTS.md'), llmWikiAgents());
|
|
30
30
|
await maybeCreate(join(base, 'wiki', 'index.md'), indexPage());
|
|
31
|
+
await maybeCreate(join(base, 'wiki', 'memory.md'), memoryPage());
|
|
31
32
|
await maybeCreate(join(base, 'wiki', 'log.md'), logPage());
|
|
32
33
|
await maybeCreate(join(base, '.gitignore'), gitignore());
|
|
33
34
|
for (const name of ['ingest.md', 'query.md', 'lint.md', 'security.md']) {
|
|
@@ -116,7 +117,7 @@ export async function appendLiveQa(projectRoot, entry) {
|
|
|
116
117
|
entry.verification || '(not captured)',
|
|
117
118
|
'',
|
|
118
119
|
'### Follow-up',
|
|
119
|
-
entry.followUp || '
|
|
120
|
+
entry.followUp || '다음 작업에서 이 turn의 reusable fact가 있으면 기존 wiki 문서에 합치고, 일회성 기록은 이 Q&A에만 보존한다.',
|
|
120
121
|
'',
|
|
121
122
|
].join('\n');
|
|
122
123
|
await appendText(path, redactText(block, 12000));
|
|
@@ -128,7 +129,7 @@ export async function writeQueryPage(projectRoot, entry) {
|
|
|
128
129
|
const slug = slugify(entry.question, 'query');
|
|
129
130
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'queries', `${day}-${slug}.md`);
|
|
130
131
|
if (await exists(path)) return path;
|
|
131
|
-
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "query"\nsource_ids: []\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\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`;
|
|
132
|
+
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "query"\nsource_ids: []\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`;
|
|
132
133
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
133
134
|
return path;
|
|
134
135
|
}
|
|
@@ -141,7 +142,7 @@ export async function writeDecisionPage(projectRoot, entry) {
|
|
|
141
142
|
const slug = slugify(entry.topic || entry.question || 'decision', 'decision');
|
|
142
143
|
const path = join(projectRoot, 'llm-wiki', 'wiki', 'decisions', `${day}-${slug}.md`);
|
|
143
144
|
if (await exists(path)) return path;
|
|
144
|
-
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "decision"\nsource_ids: []\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\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`;
|
|
145
|
+
const content = `---\ntitle: "${entry.topic || slug}"\ntype: "decision"\nsource_ids: []\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`;
|
|
145
146
|
await writeTextIfMissing(path, redactText(content, 12000));
|
|
146
147
|
return path;
|
|
147
148
|
}
|
|
@@ -153,59 +154,17 @@ export async function appendContextNote(projectRoot, eventName, text) {
|
|
|
153
154
|
await appendText(path, block);
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
function tokenize(text) {
|
|
157
|
-
return String(text || '')
|
|
158
|
-
.toLowerCase()
|
|
159
|
-
.replace(/[^\p{Letter}\p{Number}]+/gu, ' ')
|
|
160
|
-
.split(/\s+/)
|
|
161
|
-
.filter((token) => token.length >= 2)
|
|
162
|
-
.slice(0, 80);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
157
|
export async function searchWiki(projectRoot, query, limit = 5) {
|
|
166
|
-
|
|
167
|
-
const files = await listMarkdownFiles(wikiRoot, 400);
|
|
168
|
-
const terms = tokenize(query);
|
|
169
|
-
if (terms.length === 0) return [];
|
|
170
|
-
const scored = [];
|
|
171
|
-
for (const file of files) {
|
|
172
|
-
const raw = await readText(file);
|
|
173
|
-
const content = raw.slice(0, 50000);
|
|
174
|
-
const lower = content.toLowerCase();
|
|
175
|
-
let score = 0;
|
|
176
|
-
for (const term of terms) {
|
|
177
|
-
if (lower.includes(term)) score += term.length > 3 ? 2 : 1;
|
|
178
|
-
}
|
|
179
|
-
if (score > 0) {
|
|
180
|
-
const rel = relative(join(projectRoot, 'llm-wiki'), file);
|
|
181
|
-
scored.push({
|
|
182
|
-
score,
|
|
183
|
-
path: rel,
|
|
184
|
-
snippet: content.replace(/\s+/g, ' ').slice(0, 350),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
158
|
+
return searchWikiWithIndex(projectRoot, query, typeof limit === 'number' ? { limit } : limit);
|
|
189
159
|
}
|
|
190
160
|
|
|
191
161
|
export async function buildContextBrief(projectRoot, eventName, query = '') {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
'',
|
|
201
|
-
'Index excerpt:',
|
|
202
|
-
index.slice(0, 1200).trim(),
|
|
203
|
-
];
|
|
204
|
-
if (hits.length > 0) {
|
|
205
|
-
parts.push('', 'Relevant wiki pages:', ...hits.map((hit) => `- ${hit.path}: ${hit.snippet}`));
|
|
206
|
-
}
|
|
207
|
-
if (eventName === 'SessionStart') {
|
|
208
|
-
parts.push('', 'Recent log excerpt:', log.slice(-1000).trim());
|
|
209
|
-
}
|
|
210
|
-
return parts.join('\n').trim();
|
|
162
|
+
const pack = await buildContextPack(projectRoot, query, {
|
|
163
|
+
includeLog: eventName === 'SessionStart',
|
|
164
|
+
limit: 5,
|
|
165
|
+
});
|
|
166
|
+
const maintenance = await inspectProjectState(projectRoot)
|
|
167
|
+
.then(formatProjectMaintenanceContext)
|
|
168
|
+
.catch(() => '');
|
|
169
|
+
return [formatContextPack(pack), maintenance].filter(Boolean).join('\n\n');
|
|
211
170
|
}
|
package/src/state.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
|
+
import { unlink } from 'fs/promises';
|
|
2
3
|
import { kitDataDir, readJson, sha256, writeJson } from './fs-utils.js';
|
|
3
4
|
import { summarizeForStorage } from './redaction.js';
|
|
4
5
|
|
|
@@ -33,6 +34,10 @@ export async function writeTurnState(projectRoot, payload, state) {
|
|
|
33
34
|
await writeJson(statePath(projectRoot, payload), state);
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
export async function clearTurnState(projectRoot, payload) {
|
|
38
|
+
await unlink(statePath(projectRoot, payload)).catch(() => {});
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
export async function rememberQuestion(projectRoot, payload, prompt) {
|
|
37
42
|
const state = await readTurnState(projectRoot, payload);
|
|
38
43
|
const clean = summarizeForStorage(prompt, 3000);
|