llm-wiki-kit 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,8 @@ The goal is not to make users run `ingest` or `record` commands. After install,
10
10
 
11
11
  - Korean user prompt -> Korean hook guidance.
12
12
  - English user prompt -> English hook guidance.
13
+ - Codex synthetic plan execution prompts such as `Implement the plan.` are treated as neutral, so they reuse the last real session language or project preference instead of forcing English.
14
+ - Project `.kit-state.json` may set `preferredLanguage` to `ko` or `en` for neutral prompts.
13
15
  - If there is no clear current prompt language, Claude Code `settings.json` `language` is used when present.
14
16
  - If that setting is missing, local `CLAUDE.md` and `AGENTS.md` language signals are used.
15
17
  - The fallback is English.
@@ -100,7 +102,7 @@ Use Claude Code or Codex normally.
100
102
  The installed hooks:
101
103
 
102
104
  - inject functional compact context at session start, instructions loaded, and prompt submit. The hook still uses `wiki/memory.md`, `wiki/index.md`, relevant wiki search results, maintenance signals, update status, and any compact recovery packet; it formats only the useful parts so user-visible hook context does not look like a raw debug dump.
103
- - automatically choose Korean or English hook guidance from the current user prompt, then fall back to Claude Code `language`, local `CLAUDE.md`/`AGENTS.md`, and English.
105
+ - automatically choose Korean or English hook guidance from the current real user prompt, keep Codex synthetic plan execution prompts neutral, then fall back to remembered session language, project `preferredLanguage`, Claude Code `language`, local `CLAUDE.md`/`AGENTS.md`, and English.
104
106
  - remove Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` surfaces at session start so `llm-wiki/` remains the active wiki implementation
105
107
  - record small redacted raw event envelopes and per-turn state
106
108
  - capture meaningful work and structured decision points, including tool evidence, changed files, verification notes, and reusable durable-candidate signals
@@ -44,7 +44,7 @@ The hook records redacted turn summaries but does not deny tool calls only becau
44
44
 
45
45
  At `SessionStart`/`InstructionsLoaded`, the hook first attempts a safe managed-template refresh, recovers stale turn state into `outputs/maintenance/queue.md`, applies deterministic maintenance lifecycle hygiene for the current workspace, performs a cached npm update notice check for npm installs, then injects functional compact context. The context still uses `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, relevant wiki/search state, operating rules, maintenance signals, passive runtime update status, and managed-template cleanup notes; the hook formats those signals so they are usable if shown in the Claude Code UI. At `UserPromptSubmit`, it recovers stale turn state, searches wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, performs the same cached update notice check, and injects the smallest useful functional compact context set. Verbose `llm-wiki context` can explain `why selected`, `rankReason`, `matchedFields`, and `evidenceRefs`, but hook context keeps those details compact. Update notice cache is scoped by npm command, and maintenance reminders are shown for wiki/maintenance prompts, queue topic matches, approved items, durable candidates, stale/recovered items, or review-threshold pressure.
46
46
 
47
- Hook-visible language is selected from the current user prompt first. Korean prompts get Korean guidance, English prompts get English guidance. If no prompt language is clear, the hook checks Claude Code `settings.json` `language` when it exists, then local `CLAUDE.md`/`AGENTS.md` language signals, then English. The kit does not require Claude Code to expose a language setting.
47
+ Hook-visible language is selected from the current real user prompt first. Korean prompts get Korean guidance, English prompts get English guidance. Neutral prompts can reuse remembered session language or project `preferredLanguage`; then the hook checks Claude Code `settings.json` `language` when it exists, local `CLAUDE.md`/`AGENTS.md` language signals, and finally English. The kit does not require Claude Code to expose a language setting.
48
48
 
49
49
  `PostToolUse` and `PostToolBatch` record redacted tool summaries in the same turn buffer. `PreCompact` classifies the current turn before compaction: simple turns record only a context note, work-evidence, structured-decision, explicit durable, or hook-suggested durable turns write a chunked live Q&A checkpoint, and durable candidates write a maintenance queue item only when no durable wiki update is detected. Queue items carry `signal_level` and may carry safe `evidence_refs` candidates from actual repo files and verification commands. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event. `PostCompact` stores the redacted compact summary as a context note and prepares any pending recovery packet without returning model-visible context directly. In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` and `SessionEnd` append chunked live Q&A only for work-evidence, structured-decision, or durable-candidate turns and do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked to record durable knowledge, or the turn contains reusable architecture/debugging/policy/procedure/decision signals, and no durable wiki update is detected, `Stop`/`SessionEnd` queue a pending maintenance item for agent review. The lifecycle can move explicit/high-signal items to `approved`, skip stale low-signal items, and archive old reviewed items, but it never creates durable wiki pages automatically. Approved and durable-candidate maintenance items are surfaced as compact soft reminders. `Stop` and `SessionEnd` then clear the per-session turn buffer; `SubagentStop` does not.
50
50
 
@@ -31,7 +31,7 @@ Expected behavior:
31
31
 
32
32
  - `SessionStart` first attempts a safe managed-template refresh, removes Codex-facing legacy `oh-my-codex:wiki`/`omx_wiki` surfaces when they reappear, recovers stale turn state into `outputs/maintenance/queue.md`, applies deterministic maintenance lifecycle hygiene for the current workspace, performs a cached npm update notice check for npm installs, then injects functional compact context. The context still uses `llm-wiki/wiki/memory.md`, `llm-wiki/wiki/index.md`, relevant wiki/search state, operating rules, maintenance signals, passive runtime update status, and managed-template cleanup notes; the hook formats those signals so they are usable if shown in the Codex UI.
33
33
  - `UserPromptSubmit` recovers stale turn state, searches project wiki pages with MiniSearch or substring fallback, expands one-hop wikilinks, redacts context fields, performs the same cached update notice check, and injects the smallest useful functional compact context set. Verbose `llm-wiki context` can explain `why selected`, `rankReason`, `matchedFields`, and `evidenceRefs`, but hook context keeps those details compact. Update notice cache is scoped by npm command, and maintenance reminders are shown for wiki/maintenance prompts, queue topic matches, approved items, durable candidates, stale/recovered items, or review-threshold pressure.
34
- - Hook-visible language is selected from the current user prompt first. Korean prompts get Korean guidance, English prompts get English guidance. If no prompt language is clear, Codex falls back to local `CLAUDE.md`/`AGENTS.md` language signals, then English.
34
+ - Hook-visible language is selected from the current real user prompt first. Korean prompts get Korean guidance, English prompts get English guidance. Codex synthetic plan execution prompts such as `Implement the plan.` are treated as neutral, so they reuse remembered session language or project `preferredLanguage` instead of forcing English. If no prompt language is clear, Codex falls back to local `CLAUDE.md`/`AGENTS.md` language signals, then English.
35
35
  - `PreToolUse` records redacted tool summaries without blocking tool calls.
36
36
  - `PostToolUse` records redacted tool summaries in a turn buffer.
37
37
  - `PreCompact` classifies the current turn before compaction. Simple turns record only a context note; work-evidence, structured-decision, explicit durable, or hook-suggested durable turns write a chunked live Q&A checkpoint; durable candidates write a maintenance queue item only when no durable wiki update is detected. The checkpoint can include only a bounded redacted transcript tail, never the full raw transcript or raw `transcript_path`. Compaction is not blocked; if checkpoint storage fails, the hook records a compact recovery packet for the next legal context-injection event.
package/docs/manual.md CHANGED
@@ -10,6 +10,8 @@ The runtime supports seamless Korean/English use:
10
10
 
11
11
  - If the current user prompt is clearly Korean, hook-visible guidance is Korean.
12
12
  - If the current user prompt is clearly English, hook-visible guidance is English.
13
+ - Codex synthetic plan execution prompts such as `Implement the plan.` are not treated as English; they reuse the last real session language or project preference.
14
+ - A project can set `preferredLanguage` in `llm-wiki/.kit-state.json` for neutral prompts.
13
15
  - When there is no clear prompt language, Claude Code may use its `settings.json` `language` value when present.
14
16
  - If no setting exists, the runtime checks local `CLAUDE.md` and `AGENTS.md` language signals.
15
17
  - The fallback is English.
@@ -68,7 +70,7 @@ llm-wiki/
68
70
  Use Codex or Claude Code normally. Installed hooks:
69
71
 
70
72
  - inject functional compact context at session start, instructions loaded, and prompt submit;
71
- - select Korean or English hook guidance from the current user prompt and local instruction files;
73
+ - select Korean or English hook guidance from the current real user prompt, remembered session language, project preference, and local instruction files;
72
74
  - use `wiki/memory.md`, `wiki/index.md`, relevant wiki search, maintenance signals, update notices, and compact recovery packets;
73
75
  - record redacted prompt/tool/result summaries in per-turn state;
74
76
  - preserve safe evidence pointers as `evidence_refs` when changed files or verification commands are available;
@@ -296,6 +296,8 @@ Managed project files are conservative:
296
296
 
297
297
  Real registry checks require `llm-wiki-kit` to be published to npm. Before publication, `llm-wiki update --check` returns npm 404. Use fake npm in tests or local tarball install for server smoke checks.
298
298
 
299
+ For Korean-first Codex projects, set `preferredLanguage: "ko"` in `llm-wiki/.kit-state.json` when neutral prompts should stay Korean. This is especially useful after Codex plan approval, because Codex may submit `Implement the plan.` as the next prompt and the runtime deliberately treats that exact synthetic prompt as language-neutral.
300
+
299
301
  ## Release Checklist
300
302
 
301
303
  Before publishing:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-wiki-kit",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "Hook-first living Markdown wiki runtime for Codex and Claude Code with Korean/English prompt-aware guidance.",
5
5
  "type": "module",
6
6
  "files": [
@@ -3,6 +3,7 @@ import { appendWikiLog } from './project.js';
3
3
  import { backupFile, exists, readText, writeText } from './fs-utils.js';
4
4
  import { hasSecretLikeText, isSensitivePath, normalizeForStorage } from './redaction.js';
5
5
  import { indexPage, memoryPage } from './templates.js';
6
+ import { resolveLanguage } from './language.js';
6
7
  import { collectWikiPages } from './wiki-model.js';
7
8
  import { runLint } from './wiki-lint.js';
8
9
  import {
@@ -99,22 +100,25 @@ function sortedPages(pages) {
99
100
  });
100
101
  }
101
102
 
102
- function buildGeneratedMemoryBlock(pages, stats) {
103
+ function buildGeneratedMemoryBlock(pages, stats, options = {}) {
104
+ const ko = options.language === 'ko';
103
105
  const allCandidates = sortedPages(pages);
104
106
  const candidates = allCandidates.slice(0, 25);
105
107
  stats.memoryPages = candidates.length;
106
108
  const lines = [
107
- '## Generated Memory Map',
109
+ ko ? '## 생성된 Memory Map' : '## Generated Memory Map',
108
110
  '',
109
- `- Durable pages indexed: ${allCandidates.length}`,
110
- `- Hidden episodic pages: ${stats.hiddenEpisodicPages}`,
111
- `- Skipped archived/stale/superseded pages: ${stats.archivedSkippedPages}/${stats.staleSkippedPages}/${stats.supersededSkippedPages}`,
111
+ ko ? `- 색인된 durable 문서: ${allCandidates.length}` : `- Durable pages indexed: ${allCandidates.length}`,
112
+ ko ? `- 숨긴 episodic 문서: ${stats.hiddenEpisodicPages}` : `- Hidden episodic pages: ${stats.hiddenEpisodicPages}`,
113
+ ko
114
+ ? `- 건너뛴 archived/stale/superseded 문서: ${stats.archivedSkippedPages}/${stats.staleSkippedPages}/${stats.supersededSkippedPages}`
115
+ : `- Skipped archived/stale/superseded pages: ${stats.archivedSkippedPages}/${stats.staleSkippedPages}/${stats.supersededSkippedPages}`,
112
116
  ];
113
117
  if (candidates.length === 0) {
114
- lines.push('- No durable pages found yet.');
118
+ lines.push(ko ? '- 아직 durable 문서가 없습니다.' : '- No durable pages found yet.');
115
119
  return lines.join('\n');
116
120
  }
117
- lines.push('', '### High-Value Pages');
121
+ lines.push('', ko ? '### 핵심 문서' : '### High-Value Pages');
118
122
  for (const page of candidates) {
119
123
  const metadata = [
120
124
  page.type || 'unknown',
@@ -127,7 +131,8 @@ function buildGeneratedMemoryBlock(pages, stats) {
127
131
  return lines.join('\n');
128
132
  }
129
133
 
130
- function buildGeneratedIndexBlock(pages) {
134
+ function buildGeneratedIndexBlock(pages, options = {}) {
135
+ const ko = options.language === 'ko';
131
136
  const groups = new Map();
132
137
  const candidates = sortedPages(pages);
133
138
  for (const page of candidates) {
@@ -137,12 +142,12 @@ function buildGeneratedIndexBlock(pages) {
137
142
  }
138
143
 
139
144
  const lines = [
140
- '## Generated Page Map',
145
+ ko ? '## 생성된 문서 지도' : '## Generated Page Map',
141
146
  '',
142
- `- Durable pages indexed: ${candidates.length}`,
147
+ ko ? `- 색인된 durable 문서: ${candidates.length}` : `- Durable pages indexed: ${candidates.length}`,
143
148
  ];
144
149
  if (groups.size === 0) {
145
- lines.push('- No durable pages found yet.');
150
+ lines.push(ko ? '- 아직 durable 문서가 없습니다.' : '- No durable pages found yet.');
146
151
  return lines.join('\n');
147
152
  }
148
153
  for (const type of [...groups.keys()].sort()) {
@@ -171,6 +176,7 @@ export async function runConsolidate(projectRoot, options = {}) {
171
176
  const lintResult = await runLint(projectRoot, { maxFiles: options.maxFiles || 1000 });
172
177
  const pages = await collectWikiPages(projectRoot, { maxFiles: options.maxFiles || 1000 });
173
178
  const { candidates, stats } = classifyGeneratedPages(pages);
179
+ const language = await resolveLanguage(projectRoot, {}).catch(() => 'en');
174
180
  const changed = [];
175
181
  const skipped = [];
176
182
 
@@ -180,7 +186,7 @@ export async function runConsolidate(projectRoot, options = {}) {
180
186
  memoryPage(),
181
187
  MEMORY_START,
182
188
  MEMORY_END,
183
- buildGeneratedMemoryBlock(candidates, stats),
189
+ buildGeneratedMemoryBlock(candidates, stats, { language }),
184
190
  options,
185
191
  );
186
192
  if (memoryChange?.changed) changed.push(memoryChange.changed);
@@ -192,7 +198,7 @@ export async function runConsolidate(projectRoot, options = {}) {
192
198
  indexPage(),
193
199
  INDEX_START,
194
200
  INDEX_END,
195
- buildGeneratedIndexBlock(candidates),
201
+ buildGeneratedIndexBlock(candidates, { language }),
196
202
  options,
197
203
  );
198
204
  if (indexChange?.changed) changed.push(indexChange.changed);
package/src/hook.js CHANGED
@@ -6,10 +6,10 @@ import { applyMaintenanceLifecycle, recoverStaleTurnStates, recordMaintenanceFor
6
6
  import { applyProjectTemplateUpdate, inspectProjectState } from './project-state.js';
7
7
  import { recordProject } from './projects.js';
8
8
  import { summarizeForStorage } from './redaction.js';
9
- import { buildEntryFromState, clearTurnState, rememberQuestion, rememberTool } from './state.js';
9
+ import { buildEntryFromState, clearTurnState, readRememberedLanguage, rememberLanguage, rememberQuestion, rememberTool } from './state.js';
10
10
  import { updateNoticeContext } from './update-notice.js';
11
11
  import { removeLegacyOmxWikiSurfaces } from './legacy-omx-wiki.js';
12
- import { resolveLanguage } from './language.js';
12
+ import { detectTextLanguage, isSyntheticPlanPrompt, resolveLanguage } from './language.js';
13
13
  import { relative } from 'path';
14
14
 
15
15
  async function readStdinJson() {
@@ -30,6 +30,11 @@ function promptText(payload) {
30
30
  return payload.prompt || payload.user_prompt || payload.userPrompt || payload.message || payload.input || '';
31
31
  }
32
32
 
33
+ function storagePromptText(prompt, language) {
34
+ if (!isSyntheticPlanPrompt(prompt)) return prompt;
35
+ return language === 'ko' ? '승인된 계획 실행' : 'Approved plan implementation.';
36
+ }
37
+
33
38
  function toolSummary(payload) {
34
39
  const toolName = payload.tool_name || payload.toolName || payload.name || payload.tool?.name || 'tool';
35
40
  const input = payload.tool_input || payload.toolInput || payload.input || payload.arguments || payload.tool?.input || {};
@@ -145,10 +150,17 @@ export async function handleHook(provider, explicitEvent) {
145
150
  const cwd = payload.cwd || process.cwd();
146
151
  const projectRoot = await findProjectRoot(cwd);
147
152
  await bootstrapProject(projectRoot);
153
+ const prompt = eventName === 'UserPromptSubmit' ? promptText(payload) : '';
154
+ const promptLanguage = eventName === 'UserPromptSubmit' ? detectTextLanguage(prompt) : null;
155
+ const rememberedLanguage = await readRememberedLanguage(projectRoot, payload).catch(() => null);
148
156
  const requestLanguage = await resolveLanguage(projectRoot, {
149
157
  provider,
150
- prompt: eventName === 'UserPromptSubmit' ? promptText(payload) : '',
158
+ prompt,
159
+ rememberedLanguage,
151
160
  }).catch(() => 'en');
161
+ if (eventName === 'UserPromptSubmit' && promptLanguage) {
162
+ await rememberLanguage(projectRoot, payload, promptLanguage, 'prompt').catch(() => null);
163
+ }
152
164
  await recordProject(projectRoot, 'hook').catch(() => {});
153
165
  await autoUpdateManagedProject(projectRoot, eventName).catch(() => {});
154
166
  await removeLegacyWikiSurfaces(projectRoot, eventName).catch(() => {});
@@ -175,14 +187,17 @@ export async function handleHook(provider, explicitEvent) {
175
187
  }
176
188
 
177
189
  if (eventName === 'UserPromptSubmit') {
178
- const prompt = promptText(payload);
179
- await rememberQuestion(projectRoot, payload, prompt);
180
- const guidance = formatDurableCaptureGuidance(prompt, { language: requestLanguage });
190
+ const storedPrompt = storagePromptText(prompt, requestLanguage);
191
+ await rememberQuestion(projectRoot, payload, storedPrompt, {
192
+ language: requestLanguage,
193
+ skipIfExisting: isSyntheticPlanPrompt(prompt),
194
+ });
195
+ const guidance = formatDurableCaptureGuidance(storedPrompt, { language: requestLanguage });
181
196
  const recovery = await consumeCompactRecoveryContext(projectRoot, payload).catch(() => '');
182
197
  const context = await hookContext(
183
198
  projectRoot,
184
199
  eventName,
185
- [recovery, await buildContextBrief(projectRoot, eventName, prompt, { language: requestLanguage }), guidance].filter(Boolean).join('\n\n'),
200
+ [recovery, await buildContextBrief(projectRoot, eventName, storedPrompt, { language: requestLanguage }), guidance].filter(Boolean).join('\n\n'),
186
201
  payload,
187
202
  requestLanguage
188
203
  );
@@ -210,7 +225,10 @@ export async function handleHook(provider, explicitEvent) {
210
225
 
211
226
  if (eventName === 'Stop' || eventName === 'SubagentStop' || eventName === 'SessionEnd') {
212
227
  const assistantText = payload.last_assistant_message || payload.response || payload.assistant_response || '';
213
- const entry = await buildEntryFromState(projectRoot, payload, assistantText);
228
+ const entry = {
229
+ ...await buildEntryFromState(projectRoot, payload, assistantText),
230
+ language: requestLanguage,
231
+ };
214
232
  if (isLegacyEagerCaptureMode()) {
215
233
  await handleLegacyEagerStop(projectRoot, eventName, payload, entry);
216
234
  } else {
package/src/language.js CHANGED
@@ -3,6 +3,7 @@ import { homeDir, readJson, readText } from './fs-utils.js';
3
3
 
4
4
  const HANGUL_RE = /[\u3131-\u318e\uac00-\ud7a3]/g;
5
5
  const LATIN_RE = /[A-Za-z]/g;
6
+ const SYNTHETIC_PLAN_PROMPT_RE = /^\s*implement\s+the\s+plan\.?\s*$/i;
6
7
 
7
8
  export function normalizeLanguage(value) {
8
9
  const text = String(value || '').trim().toLowerCase();
@@ -14,9 +15,14 @@ export function normalizeLanguage(value) {
14
15
  return null;
15
16
  }
16
17
 
18
+ export function isSyntheticPlanPrompt(value) {
19
+ return SYNTHETIC_PLAN_PROMPT_RE.test(String(value || ''));
20
+ }
21
+
17
22
  export function detectTextLanguage(value) {
18
23
  const text = String(value || '').normalize('NFC');
19
24
  if (!text.trim()) return null;
25
+ if (isSyntheticPlanPrompt(text)) return null;
20
26
  const hangul = (text.match(HANGUL_RE) || []).length;
21
27
  const latin = (text.match(LATIN_RE) || []).length;
22
28
  if (hangul >= 2) return 'ko';
@@ -25,6 +31,11 @@ export function detectTextLanguage(value) {
25
31
  return null;
26
32
  }
27
33
 
34
+ async function projectPreferredLanguage(projectRoot) {
35
+ const state = await readJson(join(projectRoot, 'llm-wiki', '.kit-state.json'), null);
36
+ return normalizeLanguage(state?.preferredLanguage || state?.preferred_language);
37
+ }
38
+
28
39
  async function claudeSettingsLanguage(projectRoot) {
29
40
  const paths = [
30
41
  join(homeDir(), '.claude', 'settings.json'),
@@ -61,6 +72,10 @@ async function instructionFileLanguage(projectRoot) {
61
72
  export async function resolveLanguage(projectRoot, options = {}) {
62
73
  const promptLanguage = detectTextLanguage(options.prompt || '');
63
74
  if (promptLanguage) return promptLanguage;
75
+ const rememberedLanguage = normalizeLanguage(options.rememberedLanguage);
76
+ if (rememberedLanguage) return rememberedLanguage;
77
+ const preferredLanguage = await projectPreferredLanguage(projectRoot);
78
+ if (preferredLanguage) return preferredLanguage;
64
79
  if (String(options.provider || '').toLowerCase() === 'claude') {
65
80
  const settingsLanguage = await claudeSettingsLanguage(projectRoot);
66
81
  if (settingsLanguage) return settingsLanguage;
package/src/live-qa.js CHANGED
@@ -95,31 +95,52 @@ async function targetChunkPath(projectRoot, day, block, options = {}) {
95
95
  }
96
96
 
97
97
  export function formatLiveQaBlock(entry) {
98
+ const korean = entry.language === 'ko';
99
+ const labels = korean
100
+ ? {
101
+ question: '### 질문',
102
+ work: '### 작업',
103
+ result: '### 결과',
104
+ changedFiles: '### 변경 파일',
105
+ verification: '### 검증',
106
+ followUp: '### 후속 조치',
107
+ fallback: '작업/결정 중심 live Q&A 기록입니다. 재사용할 사실은 사용자가 요청했거나 명확히 중요할 때만 기존 durable wiki 문서에 병합합니다.',
108
+ }
109
+ : {
110
+ question: '### Question',
111
+ work: '### Work',
112
+ result: '### Result',
113
+ changedFiles: '### Changed files',
114
+ verification: '### Verification',
115
+ followUp: '### Follow-up',
116
+ fallback: 'Work/decision-focused live Q&A record. Merge reusable facts into existing durable wiki pages only when the user asks or the fact is clearly important.',
117
+ };
98
118
  return [
99
119
  `\n## ${timeKst()} KST - ${entry.topic || 'session turn'}`,
100
120
  '',
101
- '### Question',
121
+ labels.question,
102
122
  entry.question || '(not captured)',
103
123
  '',
104
- '### Work',
124
+ labels.work,
105
125
  entry.work || '(not captured)',
106
126
  '',
107
- '### Result',
127
+ labels.result,
108
128
  entry.result || '(not captured)',
109
129
  '',
110
- '### Changed files',
130
+ labels.changedFiles,
111
131
  entry.changedFiles || '(not captured)',
112
132
  '',
113
- '### Verification',
133
+ labels.verification,
114
134
  entry.verification || '(not captured)',
115
135
  '',
116
- '### Follow-up',
117
- entry.followUp || 'Work/decision-focused live Q&A record. Merge reusable facts into existing durable wiki pages only when the user asks or the fact is clearly important.',
136
+ labels.followUp,
137
+ entry.followUp || labels.fallback,
118
138
  '',
119
139
  ].join('\n');
120
140
  }
121
141
 
122
- export async function refreshLiveQaIndex(projectRoot, day = todayKst()) {
142
+ export async function refreshLiveQaIndex(projectRoot, day = todayKst(), options = {}) {
143
+ const korean = options.language === 'ko';
123
144
  const dayDir = liveQaDayDir(projectRoot, day);
124
145
  const files = await listChunkFiles(dayDir);
125
146
  const rows = [];
@@ -134,13 +155,15 @@ export async function refreshLiveQaIndex(projectRoot, day = todayKst()) {
134
155
  const content = [
135
156
  `# Live Q&A ${day}`,
136
157
  '',
137
- 'Chunked live Q&A archive for meaningful work and decision turns.',
158
+ korean
159
+ ? '의미 있는 작업과 결정 turn을 날짜별 chunk로 보관하는 live Q&A archive입니다.'
160
+ : 'Chunked live Q&A archive for meaningful work and decision turns.',
138
161
  '',
139
162
  `- chunks: ${files.length}`,
140
163
  `- turns: ${totalBlocks}`,
141
164
  `- updated_at: ${new Date().toISOString()}`,
142
165
  '',
143
- '## Chunks',
166
+ korean ? '## 청크' : '## Chunks',
144
167
  '',
145
168
  rows.length > 0 ? rows.join('\n') : '(none)',
146
169
  '',
@@ -153,7 +176,7 @@ export async function appendLiveQa(projectRoot, entry, options = {}) {
153
176
  const block = redactText(formatLiveQaBlock(entry), 12000);
154
177
  const path = await targetChunkPath(projectRoot, day, block, options);
155
178
  await appendText(path, block);
156
- await refreshLiveQaIndex(projectRoot, day);
179
+ await refreshLiveQaIndex(projectRoot, day, { language: entry.language });
157
180
  return path;
158
181
  }
159
182
 
package/src/project.js CHANGED
@@ -109,7 +109,27 @@ export async function writeQueryPage(projectRoot, entry) {
109
109
  const path = join(projectRoot, 'llm-wiki', 'wiki', 'queries', `${day}-${slug}.md`);
110
110
  if (await exists(path)) return path;
111
111
  const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
112
- const 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`;
112
+ const ko = entry.language === 'ko';
113
+ const labels = ko
114
+ ? {
115
+ question: '질문',
116
+ answer: '답변 요약',
117
+ work: '작업 메모',
118
+ verification: '검증',
119
+ related: '관련 문서',
120
+ changeLog: '변경 기록',
121
+ captured: 'llm-wiki-kit hook이 자동 기록함.',
122
+ }
123
+ : {
124
+ question: 'Question',
125
+ answer: 'Answer Summary',
126
+ work: 'Work Notes',
127
+ verification: 'Verification',
128
+ related: 'Related Pages',
129
+ changeLog: 'Change Log',
130
+ captured: 'Captured automatically by llm-wiki-kit hook.',
131
+ };
132
+ const content = `---\ntitle: "${entry.topic || slug}"\ntype: "query"\nsource_ids: []\n${evidenceRefs}\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\nmemory_type: "episodic"\nimportance: 2\nlast_verified: "unknown"\nsupersedes: []\nsuperseded_by: []\n---\n\n# ${entry.topic || entry.question.slice(0, 80)}\n\n## ${labels.question}\n${entry.question}\n\n## ${labels.answer}\n${entry.result || '(not captured)'}\n\n## ${labels.work}\n${entry.work || '(not captured)'}\n\n## ${labels.verification}\n${entry.verification || '(not captured)'}\n\n## ${labels.related}\n- [[index]]\n\n## ${labels.changeLog}\n- ${day}: ${labels.captured}\n`;
113
133
  await writeTextIfMissing(path, redactText(content, 12000));
114
134
  return path;
115
135
  }
@@ -123,7 +143,33 @@ export async function writeDecisionPage(projectRoot, entry) {
123
143
  const path = join(projectRoot, 'llm-wiki', 'wiki', 'decisions', `${day}-${slug}.md`);
124
144
  if (await exists(path)) return path;
125
145
  const evidenceRefs = frontmatterEvidenceRefs(evidenceRefsFromEntry(entry, { projectRoot }));
126
- const 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`;
146
+ const ko = entry.language === 'ko';
147
+ const labels = ko
148
+ ? {
149
+ title: '결정',
150
+ decision: '결정',
151
+ context: '맥락',
152
+ evidence: '근거',
153
+ verification: '검증',
154
+ openQuestions: '열린 질문',
155
+ changeLog: '변경 기록',
156
+ fallbackDecision: '(assistant 응답에서 캡처됨; 검토 필요)',
157
+ fallbackOpen: '(기록 없음)',
158
+ captured: 'llm-wiki-kit hook이 자동 기록함.',
159
+ }
160
+ : {
161
+ title: 'Decision',
162
+ decision: 'Decision',
163
+ context: 'Context',
164
+ evidence: 'Evidence',
165
+ verification: 'Verification',
166
+ openQuestions: 'Open Questions',
167
+ changeLog: 'Change Log',
168
+ fallbackDecision: '(captured from assistant response; review needed)',
169
+ fallbackOpen: '(none captured)',
170
+ captured: 'Captured automatically by llm-wiki-kit hook.',
171
+ };
172
+ const content = `---\ntitle: "${entry.topic || slug}"\ntype: "decision"\nsource_ids: []\n${evidenceRefs}\nstatus: "draft"\nlast_updated: "${day}"\nconfidence: "medium"\nmemory_type: "semantic"\nimportance: 4\nlast_verified: "unknown"\nsupersedes: []\nsuperseded_by: []\n---\n\n# ${entry.topic || labels.title}\n\n## ${labels.decision}\n${entry.result || labels.fallbackDecision}\n\n## ${labels.context}\n${entry.question || '(not captured)'}\n\n## ${labels.evidence}\n${entry.work || '(not captured)'}\n\n## ${labels.verification}\n${entry.verification || '(not captured)'}\n\n## ${labels.openQuestions}\n${entry.followUp || labels.fallbackOpen}\n\n## ${labels.changeLog}\n- ${day}: ${labels.captured}\n`;
127
173
  await writeTextIfMissing(path, redactText(content, 12000));
128
174
  return path;
129
175
  }
package/src/state.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { join } from 'path';
2
2
  import { unlink } from 'fs/promises';
3
3
  import { kitDataDir, readJson, sha256, writeJson } from './fs-utils.js';
4
+ import { normalizeLanguage } from './language.js';
4
5
  import { summarizeForStorage } from './redaction.js';
5
6
 
6
7
  export function sessionKey(projectRoot, payload) {
@@ -17,6 +18,18 @@ export function statePath(projectRoot, payload) {
17
18
  return join(kitDataDir(), 'state', `${sessionKey(projectRoot, payload)}.json`);
18
19
  }
19
20
 
21
+ function projectLanguageKey(projectRoot) {
22
+ return sha256(projectRoot).slice(0, 32);
23
+ }
24
+
25
+ export function sessionLanguagePath(projectRoot, payload) {
26
+ return join(kitDataDir(), 'language', `${sessionKey(projectRoot, payload)}.json`);
27
+ }
28
+
29
+ export function projectLanguagePath(projectRoot) {
30
+ return join(kitDataDir(), 'language', `${projectLanguageKey(projectRoot)}-project.json`);
31
+ }
32
+
20
33
  export async function readTurnState(projectRoot, payload) {
21
34
  return (await readJson(statePath(projectRoot, payload), null)) || {
22
35
  projectRoot,
@@ -54,12 +67,40 @@ export async function clearCompactRecovery(projectRoot, payload) {
54
67
  await unlink(compactRecoveryPath(projectRoot, payload)).catch(() => {});
55
68
  }
56
69
 
57
- export async function rememberQuestion(projectRoot, payload, prompt) {
70
+ export async function rememberLanguage(projectRoot, payload, language, source = 'prompt') {
71
+ const normalized = normalizeLanguage(language);
72
+ if (!normalized) return null;
73
+ const record = {
74
+ language: normalized,
75
+ source,
76
+ updated_at: new Date().toISOString(),
77
+ };
78
+ await writeJson(sessionLanguagePath(projectRoot, payload), record);
79
+ await writeJson(projectLanguagePath(projectRoot), record);
80
+ return record;
81
+ }
82
+
83
+ export async function readRememberedLanguage(projectRoot, payload) {
84
+ const session = await readJson(sessionLanguagePath(projectRoot, payload), null);
85
+ const sessionLanguage = normalizeLanguage(session?.language);
86
+ if (sessionLanguage) return sessionLanguage;
87
+ const project = await readJson(projectLanguagePath(projectRoot), null);
88
+ return normalizeLanguage(project?.language);
89
+ }
90
+
91
+ export async function rememberQuestion(projectRoot, payload, prompt, options = {}) {
58
92
  const state = await readTurnState(projectRoot, payload);
93
+ if (options.skipIfExisting && state.questions.length > 0) {
94
+ if (options.language) state.language = normalizeLanguage(options.language) || state.language;
95
+ await writeTurnState(projectRoot, payload, state);
96
+ return state;
97
+ }
59
98
  const clean = summarizeForStorage(prompt, 3000);
60
99
  if (clean) state.questions.push({ at: new Date().toISOString(), text: clean });
61
100
  state.questions = state.questions.slice(-5);
101
+ if (options.language) state.language = normalizeLanguage(options.language) || state.language;
62
102
  await writeTurnState(projectRoot, payload, state);
103
+ return state;
63
104
  }
64
105
 
65
106
  export async function rememberTool(projectRoot, payload, summary) {
@@ -112,5 +153,6 @@ export function buildEntryFromTurnState(state, assistantText = '') {
112
153
  followUp: '',
113
154
  firstTimestamp: questions[0]?.at || tools[0]?.at || state.updated_at,
114
155
  session: state.session,
156
+ language: normalizeLanguage(state.language),
115
157
  };
116
158
  }
package/src/templates.js CHANGED
@@ -9,7 +9,7 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
9
9
 
10
10
  - This block supersedes older OMX/OMC/\`omx_wiki/\` LLM Wiki instructions for this repository.
11
11
  - Use Codex or Claude Code normally. Do not create a workflow where users must remember extra \`llm-wiki\` commands during ordinary work.
12
- - Language: respond in the user's current prompt language when it is clearly Korean or English. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
12
+ - Language: respond in the user's current real prompt language when it is clearly Korean or English. Codex synthetic plan execution prompts such as \`Implement the plan.\` are neutral and should reuse the last real session language or project preference. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
13
13
  - Treat chat memory as temporary. Durable project knowledge belongs in repository Markdown under \`llm-wiki/\`.
14
14
  - \`llm-wiki/raw/\` is the immutable or redacted evidence layer. Do not modify raw captures except safe hook envelope append.
15
15
  - \`llm-wiki/wiki/\` is the curated knowledge layer managed by the agent. Put decisions, architecture, debugging findings, concepts, procedures, and context there.
@@ -46,7 +46,8 @@ The user should not need to run many \`llm-wiki\` commands manually. The agent c
46
46
  These rules replace older OMX/OMC/\`omx_wiki/\` rules for this project.
47
47
 
48
48
  ## Language
49
- - Follow the user's current prompt language when it is clearly Korean or English.
49
+ - Follow the user's current real prompt language when it is clearly Korean or English.
50
+ - Treat Codex synthetic plan execution prompts such as \`Implement the plan.\` as neutral; reuse the last real session language or project preference when available.
50
51
  - If no current prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
51
52
  - Keep commands, paths, code identifiers, API names, package names, logs, and original error text unchanged.
52
53
  - Korean and English users should both be able to use Codex/Claude Code naturally without changing \`llm-wiki\` commands.
@@ -470,16 +470,16 @@ export function formatHookContextPack(pack, options = {}) {
470
470
  const language = options.language === 'ko' ? 'ko' : 'en';
471
471
  const lines = language === 'ko'
472
472
  ? [
473
- 'LLM Wiki context (functional compact):',
473
+ 'LLM Wiki context (기능 요약):',
474
474
  '- 우선순위: 현재 사용자 요청에 먼저 답하고, wiki context는 도움이 될 때만 사용한다.',
475
475
  '- 언어: 현재 사용자 입력 언어를 따른다. 경로, 명령어, 코드 식별자, 오류 원문은 원래 언어를 유지한다.',
476
- '- 기능 유지: hook memory/search/capture/maintenance/update signals are active.',
476
+ '- 기능 유지: hook memory/search/capture/maintenance/update 신호가 활성화되어 있다.',
477
477
  '- 오래 남길 프로젝트 지식은 llm-wiki/wiki에 둔다. credentials, tokens, private keys, .env contents는 저장하지 않는다.',
478
478
  ]
479
479
  : [
480
480
  'LLM Wiki context (functional compact):',
481
481
  '- Priority: answer the current user request first; use wiki context when it helps.',
482
- '- Language: follow the current user prompt language. Keep paths, commands, code identifiers, and original error text unchanged.',
482
+ '- Language: follow the current real user prompt language. Keep paths, commands, code identifiers, and original error text unchanged.',
483
483
  '- Functionality: hook memory/search/capture/maintenance/update signals are active.',
484
484
  '- Durable project knowledge belongs in llm-wiki/wiki; never store credentials, tokens, private keys, or .env contents.',
485
485
  ];
@@ -490,12 +490,12 @@ export function formatHookContextPack(pack, options = {}) {
490
490
 
491
491
  const memoryLines = memoryFocusLines(pack.memoryExcerpt, options.memoryLineLimit ?? 6);
492
492
  if (memoryLines.length > 0) {
493
- lines.push('', language === 'ko' ? 'Memory focus:' : 'Memory focus:');
493
+ lines.push('', language === 'ko' ? '메모리 핵심:' : 'Memory focus:');
494
494
  lines.push(...memoryLines.map((line) => `- ${line.replace(/^-\s+/, '')}`));
495
495
  } else {
496
496
  lines.push(
497
497
  '',
498
- 'Memory focus:',
498
+ language === 'ko' ? '메모리 핵심:' : 'Memory focus:',
499
499
  language === 'ko'
500
500
  ? '- 프로젝트 맥락이 필요하면 llm-wiki/wiki/memory.md와 llm-wiki/wiki/index.md를 확인한다.'
501
501
  : '- See llm-wiki/wiki/memory.md and llm-wiki/wiki/index.md when project context is needed.'
@@ -504,7 +504,7 @@ export function formatHookContextPack(pack, options = {}) {
504
504
 
505
505
  const hits = hookHits(pack, options);
506
506
  if (hits.length > 0) {
507
- lines.push('', language === 'ko' ? 'Relevant wiki pages:' : 'Relevant wiki pages:');
507
+ lines.push('', language === 'ko' ? '관련 wiki 문서:' : 'Relevant wiki pages:');
508
508
  for (const hit of hits) {
509
509
  const suffix = hit.source === 'linked' && hit.via.length > 0
510
510
  ? `, via ${hit.via.join(', ')}`
@@ -514,7 +514,7 @@ export function formatHookContextPack(pack, options = {}) {
514
514
  } else if (pack.query) {
515
515
  lines.push(
516
516
  '',
517
- 'Relevant wiki pages:',
517
+ language === 'ko' ? '관련 wiki 문서:' : 'Relevant wiki pages:',
518
518
  language === 'ko'
519
519
  ? '- 직접 관련된 durable wiki 문서를 찾지 못했다. 정상적으로 답하고, 필요할 때만 파일/wiki를 확인한다.'
520
520
  : '- No directly relevant durable wiki page found; answer normally and inspect files/wiki only if needed.'
@@ -523,7 +523,7 @@ export function formatHookContextPack(pack, options = {}) {
523
523
 
524
524
  const logLines = logFocusLines(pack.logExcerpt, options.logLineLimit ?? 2);
525
525
  if (logLines.length > 0) {
526
- lines.push('', 'Recent log:');
526
+ lines.push('', language === 'ko' ? '최근 로그:' : 'Recent log:');
527
527
  lines.push(...logLines.map((line) => `- ${line.replace(/^-\s+/, '')}`));
528
528
  }
529
529