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.
@@ -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
- marker: 'Generated by llm-wiki-kit',
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 hasGeneratedMarker(text, descriptor) {
85
- return descriptor.marker ? text.includes(descriptor.marker) : false;
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 !recorded.hash || recorded.hash === templateHash(currentText);
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) || hasGeneratedMarker(text, descriptor);
291
+ return templateHash(text) === templateHash(descriptor.content);
98
292
  }
99
293
 
100
- function canPatchDescriptor(state, descriptor, currentText) {
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) || isKnownGeneratedContent(currentText, descriptor);
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 = fileExists ? desiredTextForDescriptor(descriptor, currentText) : descriptor.content;
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 patchable = fileExists ? canPatchDescriptor(state, descriptor, currentText) : false;
124
- const current = fileExists && desiredText !== null && currentHash === templateHash(desiredText);
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 || '(none captured)',
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
- const wikiRoot = join(projectRoot, 'llm-wiki', 'wiki');
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 index = await readText(join(projectRoot, 'llm-wiki', 'wiki', 'index.md'));
193
- const log = await readText(join(projectRoot, 'llm-wiki', 'wiki', 'log.md'));
194
- const hits = query ? await searchWiki(projectRoot, query, 5) : [];
195
- const parts = [
196
- 'LLM Wiki context from llm-wiki-kit:',
197
- '- Treat chat memory as temporary; update project Markdown when knowledge should persist.',
198
- '- Preserve raw/wiki separation. Do not store secrets, tokens, .env contents, private keys, or personal/customer identifiers.',
199
- '- Prefer updating existing wiki pages over creating duplicate pages.',
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);