llm-wiki-kit 0.2.17 → 0.2.19

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,19 +102,19 @@ 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
107
- - attach safe `evidence_refs` candidates to generated durable candidates when changed files or verification commands are available
108
- - before compaction, classify the current turn and save a redacted checkpoint only for meaningful work, structured decisions, explicit durable requests, or suggested durable candidates; durable candidates get a maintenance queue item when no durable wiki update is detected
109
+ - attach safe `evidence_refs` candidates to generated queue items when changed files or verification commands are available
110
+ - before compaction, classify the current turn and save a redacted checkpoint only for meaningful work, structured decisions, explicit durable requests, or recovered work; explicit/structured durable candidates get a maintenance queue item when no durable wiki update is detected
109
111
  - after compaction, store the redacted compact summary only; if pre-compact preservation failed, prepare a recovery packet for the next legal model-visible context hook
110
112
  - allow tool calls to proceed without secret/PII-based hook blocking
111
- - update chunked `llm-wiki/outputs/questions/YYYY-MM-DD/live-qa-001.md` style archives only for meaningful work, structured decision turns, or hook-suggested durable candidates
113
+ - update chunked `llm-wiki/outputs/questions/YYYY-MM-DD/live-qa-001.md` style archives only for meaningful work, structured decision turns, or explicit durable requests
112
114
  - avoid automatic `wiki/queries/` and `wiki/decisions/` promotion in the default answer-first mode
113
- - queue durable cleanup candidates for explicit documentation requests, hook-suggested durable candidates, or recovered stale turn state that were not reflected in durable wiki files
115
+ - queue durable cleanup candidates for explicit documentation requests, structured durable conclusions, or recovered stale turn state that were not reflected in durable wiki files
114
116
  - recover stale per-turn state into that queue on the next session start or prompt submit when the previous stop hook did not complete
115
- - nudge the active LLM to fold approved or hook-suggested reusable facts into existing wiki pages instead of leaving everything as one-off Q&A
117
+ - nudge the active LLM to batch-review accumulated candidates and fold only explicit, repeated, or clearly reusable facts into existing wiki pages
116
118
  - automatically refresh managed rules/templates for older projects when the current runtime starts a session
117
119
 
118
120
  If you need to think about saving every answer manually, the setup has failed.
@@ -148,7 +150,7 @@ Installed npm runtimes also perform a cached update notice check from hooks whil
148
150
 
149
151
  `llm-wiki consolidate` refreshes only generated marker blocks in `wiki/memory.md` and `wiki/index.md`. Generated maps keep durable non-archived pages, hide default episodic records, skip stale/archived/superseded pages, and report those counts in dry-run output. It is an agent maintenance helper, not a command users should run after every turn.
150
152
 
151
- `llm-wiki maintenance` prints the queue and review due status from `llm-wiki/outputs/maintenance/queue.md`. Queue states are `pending -> approved -> done` or `skipped`. Use `llm-wiki maintenance --workspace <project> --approve <id> --target <wiki/...md>` when durable promotion is approved, `--done <id> --target <wiki/...md>` after the active agent has merged the fact into a durable page, and `--skip <id> [--note "..."]` for duplicate or non-durable candidates. `llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run` previews automatic queue hygiene; without `--dry-run`, high-signal or explicit candidates are moved to `approved` once pending reaches the lifecycle threshold, old low-signal candidates may be skipped, and old reviewed items move to `outputs/maintenance/archive/`. Hooks apply that lifecycle only on `SessionStart`/`InstructionsLoaded`; `UserPromptSubmit` only reminds. Approved items are shown before pending items in hook reminders. Periodic maintenance is a soft agent-side reminder, not a user command loop.
153
+ `llm-wiki maintenance` prints the queue and review due status from `llm-wiki/outputs/maintenance/queue.md`. Queue states are `pending -> approved -> done` or `skipped`. Use `llm-wiki maintenance --workspace <project> --approve <id> --target <wiki/...md>` only when a human/agent explicitly accepts durable promotion, `--done <id> --target <wiki/...md>` after the active agent has merged the fact into a durable page, and `--skip <id> [--note "..."]` for duplicate or non-durable candidates. `llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run` previews automatic queue hygiene; without `--dry-run`, candidates remain pending for batch review, old low-signal candidates may be skipped, and old reviewed items move to `outputs/maintenance/archive/`. Hooks apply that lifecycle only on `SessionStart`/`InstructionsLoaded`; `UserPromptSubmit` only reminds for explicit documentation, recovered state, maintenance prompts, or batch thresholds. Approved items are shown before pending items in hook reminders. Periodic maintenance is a soft agent-side reminder, not a user command loop.
152
154
 
153
155
  `llm-wiki eval --workspace <project> [--fixture <path>] [--limit 5] [--json]` runs retrieval fixtures from `llm-wiki/evals/retrieval.json` by default. If the fixture is absent, it exits successfully with `no fixture found`. Fixtures list `query`, `expected`, and `unexpected` paths; output reports recall, missed expected hits, unexpected hits, and top hits using the same durable visibility policy as export.
154
156
 
package/docs/concepts.md CHANGED
@@ -18,13 +18,13 @@ The important behavior is a loop:
18
18
  2. `memory.md`, `index.md`, and relevant wiki context are injected automatically with an answer-first instruction.
19
19
  3. The user works normally; no extra command loop is required.
20
20
  4. Hooks gather redacted prompt/tool/result summaries.
21
- 5. At stop/session end, hooks append redacted chunked live Q&A only for turns with work evidence, structured decision/debugging conclusions, or hook-suggested durable candidates.
21
+ 5. At stop/session end, hooks append redacted chunked live Q&A only for turns with work evidence, structured decision/debugging conclusions, or explicit durable requests.
22
22
  6. When possible, generated candidates carry safe `evidence_refs` such as changed files, verification commands, raw source IDs, or external URLs.
23
23
  7. Simple answers, status checks, and keyword-only responses stay out of live Q&A and durable wiki by default.
24
- 8. Durable wiki promotion is selective: explicit record/document requests and hook-suggested durable candidates should be handled by the active agent in existing wiki pages; the hook queues review only when the turn was not reflected in durable files.
24
+ 8. Durable wiki promotion is selective: explicit record/document requests and structured durable conclusions enter the queue, but the active agent should batch-review and merge only explicit, repeated, or clearly reusable facts into existing wiki pages.
25
25
  9. At the next start/prompt after an abrupt shutdown, hooks can recover stale turn state into `outputs/maintenance/queue.md`.
26
- 10. At session start, deterministic lifecycle hygiene may approve explicit/high-signal pending items, skip old low-signal items, and archive old reviewed items without generating wiki pages automatically.
27
- 11. When reusable knowledge appears, the active Claude Code/Codex agent folds approved or hook-suggested facts into existing durable wiki pages instead of leaving everything as one-off Q&A.
26
+ 10. At session start, deterministic lifecycle hygiene keeps candidates pending for batch review, may skip old low-signal items, and archives old reviewed items without generating wiki pages automatically.
27
+ 11. When reusable knowledge appears repeatedly or explicitly, the active Claude Code/Codex agent folds it into existing durable wiki pages instead of creating one page per candidate.
28
28
  12. Export and eval reuse the same durable visibility policy so handoff manifests, retrieval fixtures, and context selection describe the same wiki surface.
29
29
  13. Future sessions start from the improved wiki instead of relying on long chat history.
30
30
 
@@ -45,9 +45,9 @@ The maintenance loop is intentionally layered:
45
45
  - `index.md`: broad navigation map.
46
46
  - MiniSearch + wikilinks: retrieval over durable `wiki/**/*.md`, with episodic `wiki/queries/`, `wiki/context/`, and `session-log` pages hidden by default unless promoted or `--include-episodic` is requested; archived/superseded pages stay preserved but hidden unless `--include-archived` is requested. Verbose context explains `why selected`; hook context stays compact.
47
47
  - `evidence_refs`: optional frontmatter that ties durable claims to `file:`, `cmd:`, `raw:`, or `url:` evidence without embedding secrets or raw transcripts.
48
- - `outputs/maintenance/queue.md`: selective reminders for explicit durable requests, hook-suggested durable candidates, and stale turn recovery that need review. Queue state is `pending`, `approved`, `done`, or `skipped`; reviewed history can move to `outputs/maintenance/archive/`.
48
+ - `outputs/maintenance/queue.md`: selective reminders for explicit durable requests, structured durable conclusions, and stale turn recovery that need batch review. Queue state is `pending`, `approved`, `done`, or `skipped`; reviewed history can move to `outputs/maintenance/archive/`.
49
49
  - `lint`: finds broken links, stale pages, duplicates, metadata gaps, invalid evidence refs, secret-like content, outdated managed rules, memory/page-count budget pressure, hidden episodic growth, and stale/archived discoverability gaps.
50
- - `maintenance`: reports `reviewDue` only when periodic thresholds are met; hook reminders are soft and limited to session start/instructions loaded or compact prompt-time reminders for maintenance prompts, approved items, durable candidates, stale/recovered items, or review-threshold pressure. `--apply-lifecycle` previews or applies bounded queue hygiene; it does not merge or create durable wiki pages.
50
+ - `maintenance`: reports `reviewDue` only when periodic thresholds are met; hook reminders are soft and limited to session start/instructions loaded or compact prompt-time reminders for maintenance prompts, approved items, explicit durable requests, recovered items, or batch-threshold pressure. `--apply-lifecycle` previews or applies bounded queue hygiene; it does not merge, auto-approve, or create durable wiki pages.
51
51
  - `consolidate`: agent helper that refreshes generated blocks in `memory.md` and `index.md` while preserving handwritten notes, keeping default query/context/session pages out of the durable generated maps, and skipping stale/archived/superseded pages.
52
52
  - `eval`: checks retrieval fixtures in `llm-wiki/evals/retrieval.json` and reports expected recall, missed expected paths, unexpected hits, and top hits.
53
53
  - `export`: writes redacted `llms.txt`, `llms-full.txt`, and `llm-wiki.json` manifests for agent onboarding, handoff, retrieval eval, and external consumption. `llms.txt` is not treated as a passive SEO artifact.
@@ -42,11 +42,11 @@ when no project `CLAUDE.md` exists. Existing `CLAUDE.md` files are not overwritt
42
42
 
43
43
  The hook records redacted turn summaries but does not deny tool calls only because an input looks sensitive. Hook payloads are stored as small redacted event envelopes rather than full transcripts, and context output is redacted field by field before it is returned to Claude Code.
44
44
 
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.
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 only for wiki/maintenance prompts, approved items, explicit durable requests, recovered items, or batch 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
- `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.
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 recovered turns write a chunked live Q&A checkpoint, and explicit/structured 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 explicit durable turns and do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked to record durable knowledge, or the turn contains a structured durable conclusion, and no durable wiki update is detected, `Stop`/`SessionEnd` queues a pending maintenance item for batch review. The lifecycle keeps candidates pending for batch review, skips stale low-signal items, and archives old reviewed items, but it never creates durable wiki pages automatically or auto-approves promotion. Prompt-time reminders are limited to explicit durable requests, recovered state, maintenance prompts, approved items, or batch thresholds. `Stop` and `SessionEnd` then clear the per-session turn buffer; `SubagentStop` does not.
50
50
 
51
51
  For handoff or retrieval verification, use `llm-wiki export --workspace <project> --format all` and `llm-wiki eval --workspace <project>`. The generated `llms.txt`/`llms-full.txt`/`llm-wiki.json` files are redacted durable manifests, not raw transcripts.
52
52
 
@@ -30,14 +30,14 @@ Handled events:
30
30
  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
- - `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.
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 only for wiki/maintenance prompts, approved items, explicit durable requests, recovered items, or batch review threshold pressure.
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
- - `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.
37
+ - `PreCompact` classifies the current turn before compaction. Simple turns record only a context note; work-evidence, structured-decision, explicit durable, or recovered turns write a chunked live Q&A checkpoint; explicit/structured 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.
38
38
  - `PostCompact` stores the redacted compact summary as a context note and prepares any pending compact recovery packet. It does not return `hookSpecificOutput.additionalContext`, because Codex `PostCompact` only supports common output fields.
39
39
  - In the default `answer-first` mode, `SubagentStop` does not create live Q&A, query, decision, or maintenance files. `Stop` appends chunked live Q&A only for work-evidence, structured-decision, or durable-candidate turns and does not auto-create `wiki/queries/` or `wiki/decisions/`.
40
- - 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` queues a pending maintenance item for agent review. Queue items carry `signal_level` and may carry safe `evidence_refs` candidates from actual repo files and verification commands. 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.
40
+ - If the user explicitly asked to record durable knowledge, or the turn contains a structured durable conclusion, and no durable wiki update is detected, `Stop` queues a pending maintenance item for batch review. Queue items carry `signal_level` and may carry safe `evidence_refs` candidates from actual repo files and verification commands. The lifecycle keeps candidates pending for batch review, skips stale low-signal items, and archives old reviewed items, but it never creates durable wiki pages automatically or auto-approves promotion. Prompt-time reminders are limited to explicit durable requests, recovered state, maintenance prompts, approved items, or batch thresholds.
41
41
  - `Stop` clears the per-session turn buffer after recording. `SubagentStop` leaves the parent turn buffer available for the final stop event.
42
42
 
43
43
  Hook payloads are stored as small redacted event envelopes rather than full transcripts. Context output is also redacted field by field before it is returned to Codex. Functional compact context is a presentation policy, not a feature reduction: Codex still receives the wiki memory, search, maintenance, and passive update signals needed for the hook workflow.
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,14 +70,14 @@ 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;
75
- - archive only meaningful work turns, structured decision/debugging turns, or hook-suggested durable candidates into chunked `outputs/questions/YYYY-MM-DD/live-qa-001.md` files;
77
+ - archive only meaningful work turns, structured decision/debugging turns, or explicit durable requests into chunked `outputs/questions/YYYY-MM-DD/live-qa-001.md` files;
76
78
  - avoid automatic `wiki/queries/` and `wiki/decisions/` promotion in the default answer-first mode;
77
- - queue durable cleanup candidates for explicit documentation requests, hook-suggested durable candidates, or recovered stale turn state that were not reflected in durable wiki files;
78
- - apply deterministic queue lifecycle hygiene at session start so explicit/high-signal candidates can move to `approved`, stale low-signal candidates can be skipped, and old reviewed items can be archived without creating wiki pages automatically;
79
+ - queue durable cleanup candidates for explicit documentation requests, structured durable conclusions, or recovered stale turn state that were not reflected in durable wiki files;
80
+ - apply deterministic queue lifecycle hygiene at session start so candidates stay pending for batch review, stale low-signal candidates can be skipped, and old reviewed items can be archived without creating wiki pages automatically;
79
81
  - refresh clearly managed rules/templates for older projects at session start;
80
82
  - remove legacy Codex-facing `oh-my-codex:wiki`/`omx_wiki` surfaces when they reappear.
81
83
 
@@ -89,7 +91,7 @@ Default:
89
91
  LLM_WIKI_KIT_CAPTURE_MODE=answer-first
90
92
  ```
91
93
 
92
- `answer-first` keeps simple Q&A, status checks, and keyword-only replies out of durable wiki and live Q&A by default. It archives work turns with tool evidence, changed-file evidence, verification, structured `Decision:`/`Root cause:` style conclusions, or reusable durable-candidate signals. Explicit durable requests and hook-suggested durable candidates create maintenance queue candidates only when no durable wiki update is detected.
94
+ `answer-first` keeps simple Q&A, status checks, and keyword-only replies out of durable wiki and live Q&A by default. It archives work turns with tool evidence, changed-file evidence, verification, or structured `Decision:`/`Root cause:` style conclusions. Explicit durable requests and structured durable conclusions create maintenance queue candidates only when no durable wiki update is detected.
93
95
 
94
96
  Deprecated compatibility mode:
95
97
 
@@ -229,7 +231,7 @@ Missing fixtures exit successfully with `no fixture found`. Present fixtures rep
229
231
 
230
232
  ## Maintenance
231
233
 
232
- `llm-wiki maintenance` reports queue state and review health. It does not merge pages automatically. The active agent should merge reusable items into existing durable pages and mark queue items through `pending`, `approved`, `done`, or `skipped`.
234
+ `llm-wiki maintenance` reports queue state and review health. It does not merge pages automatically. The active agent should batch-review candidates, merge only explicit, repeated, or clearly reusable facts into existing durable pages, and mark queue items through `pending`, `approved`, `done`, or `skipped`.
233
235
 
234
236
  ```bash
235
237
  llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run
@@ -240,12 +242,12 @@ llm-wiki maintenance --workspace <project> --skip <id> --note "duplicate"
240
242
 
241
243
  `approved` means durable promotion is accepted but not yet merged. `done` means the durable page has been updated. `skipped` means the item was duplicate or not reusable enough. Approved reminders are shown before pending reminders.
242
244
 
243
- Lifecycle hygiene applies when pending reaches 15: explicit/high-signal candidates move to `approved`, old low-signal candidates may move to `skipped`, explicit requests and recovered `result_missing` items are not auto-skipped, and old reviewed items beyond the latest 10 move to `outputs/maintenance/archive/YYYY-MM.md`. Hooks apply this only on `SessionStart`/`InstructionsLoaded`; `UserPromptSubmit` only reminds.
245
+ Lifecycle hygiene applies when pending reaches 15: candidates remain pending for batch review, old low-signal candidates may move to `skipped`, explicit requests and recovered `result_missing` items are not auto-skipped, and old reviewed items beyond the latest 10 move to `outputs/maintenance/archive/YYYY-MM.md`. Hooks apply this only on `SessionStart`/`InstructionsLoaded`; `UserPromptSubmit` only reminds.
244
246
 
245
247
  Hook reminders are soft:
246
248
 
247
249
  - session start and instructions loaded may show a one-item summary;
248
- - prompt submit shows one compact reminder for wiki/maintenance prompts, queue topic matches, approved items, durable candidates, stale/recovered items, or review-threshold pressure.
250
+ - prompt submit shows one compact reminder for wiki/maintenance prompts, approved items, explicit durable requests, recovered items, or the batch review threshold.
249
251
 
250
252
  ## PreCompact
251
253
 
@@ -147,7 +147,7 @@ After a plain `npm install -g llm-wiki-kit@latest`, existing hooks keep working
147
147
 
148
148
  Daily use should be Claude Code/Codex first. The user should not need to run a chain of `llm-wiki` commands while working. Hooks inject context automatically, but the current user answer takes priority over wiki cleanup. The active agent updates durable wiki pages when reusable project knowledge appears and the turn's importance or user consent justifies persistence. Hook context policy is function-first: memory, search, maintenance, and update signals remain available, while user-visible context is formatted as functional compact context instead of a raw dump.
149
149
 
150
- In the default `LLM_WIKI_KIT_CAPTURE_MODE=answer-first` mode, `Stop` and `SessionEnd` append live Q&A only for meaningful work evidence, structured decision turns, or reusable durable-candidate signals. Simple answers, status checks, and keyword-only responses are not archived. Live Q&A uses chunked files under `llm-wiki/outputs/questions/YYYY-MM-DD/` and rolls over by line/byte budget. Hooks do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked for recording/documentation, or the turn contains reusable architecture/debugging/policy/procedure/decision signals, and no durable wiki update is detected, a pending cleanup candidate is written to `llm-wiki/outputs/maintenance/queue.md` with a local `signal_level` such as `explicit`, `high`, `low`, or `recovered`. `PreCompact` performs the same answer-first classification before context compaction: simple turns get only a context note, archive-worthy turns get a live Q&A checkpoint, and durable candidates get a checkpoint plus queue item only when needed. If checkpoint storage fails, compaction still proceeds and the hook prepares an important-only compact recovery packet for the next legal context-injection event. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete. `SessionStart` injects a one-item queue summary and applies deterministic queue lifecycle hygiene; `UserPromptSubmit` injects a compact soft reminder when the prompt is wiki/maintenance related, matches a queue topic, has approved or durable-candidate items, or the queue crosses the review threshold. This is a recovery and reminder layer, not a full transcript capture path.
150
+ In the default `LLM_WIKI_KIT_CAPTURE_MODE=answer-first` mode, `Stop` and `SessionEnd` append live Q&A only for meaningful work evidence, structured decision turns, or explicit durable requests. Simple answers, status checks, keyword-only responses, and ordinary durable keywords such as "root cause" inside work summaries are not promoted into durable review by themselves. Live Q&A uses chunked files under `llm-wiki/outputs/questions/YYYY-MM-DD/` and rolls over by line/byte budget. Hooks do not auto-create `wiki/queries/` or `wiki/decisions/`. If the user explicitly asked for recording/documentation, or the turn contains a structured durable conclusion such as `Decision:` or `Root cause:`, and no durable wiki update is detected, a pending cleanup candidate is written to `llm-wiki/outputs/maintenance/queue.md` with a local `signal_level` such as `explicit`, `high`, `low`, or `recovered`. `PreCompact` performs the same answer-first classification before context compaction: simple turns get only a context note, archive-worthy turns get a live Q&A checkpoint, and explicit/structured durable candidates get a checkpoint plus queue item only when needed. If checkpoint storage fails, compaction still proceeds and the hook prepares an important-only compact recovery packet for the next legal context-injection event. `SessionStart` and `UserPromptSubmit` also recover stale per-turn state into the same queue when the previous stop hook did not complete. `SessionStart` injects a one-item queue summary and applies deterministic queue lifecycle hygiene; `UserPromptSubmit` injects a compact soft reminder only for wiki/maintenance prompts, approved items, explicit durable requests, recovered items, or when the queue crosses the batch review threshold. This is a recovery and reminder layer, not a full transcript capture path.
151
151
 
152
152
  Use `llm-wiki archive-questions --workspace <project> --dry-run` to review splitting legacy `outputs/questions/YYYY-MM-DD-live-qa.md` files into the chunked layout. Running it without `--dry-run` preserves the original under `outputs/questions/archive/originals/` with a checksum sidecar and replaces the legacy file with a pointer stub.
153
153
 
@@ -210,11 +210,11 @@ Broken links, invalid source IDs, and secret-like content are errors and return
210
210
 
211
211
  Agents may run `consolidate` after meaningful wiki growth. Users should not need to run it after every turn.
212
212
 
213
- `llm-wiki maintenance --workspace <project>` prints queue counts, review due status, and the first pending items. It does not merge wiki pages by itself; the active agent should review pending or approved items, update the closest existing durable wiki document, then mark the queue item `done` or `skipped`. Periodic maintenance is an agent-side task, not something users need to run after every turn.
213
+ `llm-wiki maintenance --workspace <project>` prints queue counts, review due status, and the first pending items. It does not merge wiki pages by itself; the active agent should batch-review pending or approved items, update the closest existing durable wiki document only for explicit, repeated, or clearly reusable facts, then mark the queue item `done` or `skipped`. Periodic maintenance is an agent-side task, not something users need to run after every turn.
214
214
 
215
- `llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run` previews automatic queue hygiene. Running it without `--dry-run` approves explicit/high-signal pending candidates when pending reaches 15, skips low-signal pending candidates older than 14 days, never auto-skips explicit requests or recovered `result_missing` items, and moves old `done`/`skipped` items beyond the latest 10 into `outputs/maintenance/archive/YYYY-MM.md`. The lifecycle never writes durable wiki pages automatically; it only keeps the review queue bounded and prioritized. Hooks run the same lifecycle on `SessionStart`/`InstructionsLoaded`, while `UserPromptSubmit` only surfaces reminders.
215
+ `llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run` previews automatic queue hygiene. Running it without `--dry-run` keeps explicit/high-signal pending candidates pending for batch review when pending reaches 15, skips low-signal pending candidates older than 14 days, never auto-skips explicit requests or recovered `result_missing` items, and moves old `done`/`skipped` items beyond the latest 10 into `outputs/maintenance/archive/YYYY-MM.md`. The lifecycle never writes durable wiki pages automatically and never auto-approves page promotion; it only keeps the review queue bounded and prioritized. Hooks run the same lifecycle on `SessionStart`/`InstructionsLoaded`, while `UserPromptSubmit` only surfaces reminders.
216
216
 
217
- `llm-wiki maintenance --workspace <project> --json` includes `reviewDue`, `reviewReasons`, `pendingCount`, `stalePendingCount`, `health`, `recommendedCommands`, and lifecycle action counts when lifecycle is applied. Review is due when the last review is older than 14 days, pending queue size reaches 5, stale or result-missing pending items exist, lint has warnings/errors, `memory.md` is near budget, or wiki page count reaches 80% of the search cap. Hook reminders are soft: `SessionStart`/`InstructionsLoaded` may show a short due note, while `UserPromptSubmit` shows one compact item for wiki/maintenance prompts, approved items, durable candidates, stale/recovered items, or review-threshold pressure. The reminder never blocks the current answer.
217
+ `llm-wiki maintenance --workspace <project> --json` includes `reviewDue`, `reviewReasons`, `pendingCount`, `stalePendingCount`, `health`, `recommendedCommands`, and lifecycle action counts when lifecycle is applied. Review is due when the last review is older than 14 days, pending queue size reaches 15, stale or result-missing pending items exist, lint has warnings/errors, `memory.md` is near budget, or wiki page count reaches 80% of the search cap. Hook reminders are soft: `SessionStart`/`InstructionsLoaded` may show a short due note, while `UserPromptSubmit` shows one compact item only for wiki/maintenance prompts, approved items, explicit durable requests, recovered items, or batch review threshold pressure. The reminder never blocks the current answer.
218
218
 
219
219
  Recommended agent checklist:
220
220
 
@@ -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:
@@ -229,7 +229,7 @@ Check:
229
229
 
230
230
  ## Maintenance Queue Is Empty Or Stale
231
231
 
232
- In the default answer-first mode, `llm-wiki/outputs/maintenance/queue.md` is created when a user explicitly asked for durable recording/documentation, a turn was classified as a hook-suggested durable candidate, or stale per-turn state is recovered, and no durable wiki update was detected. It is not expected to grow after every normal `Stop`.
232
+ In the default answer-first mode, `llm-wiki/outputs/maintenance/queue.md` is created when a user explicitly asked for durable recording/documentation, a turn has a structured durable conclusion, or stale per-turn state is recovered, and no durable wiki update was detected. It is not expected to grow after every normal `Stop`, and ordinary durable words inside a work summary are not enough by themselves.
233
233
 
234
234
  Check the queue and health warnings:
235
235
 
@@ -239,7 +239,7 @@ llm-wiki maintenance --workspace /path/to/project --apply-lifecycle --dry-run
239
239
  llm-wiki lint --workspace /path/to/project
240
240
  ```
241
241
 
242
- If the queue is always empty during ordinary Q&A, that is normal. If you expected an explicit documentation request or durable candidate to queue, confirm hooks run and that the turn had a captured `UserPromptSubmit`. If pending items stay around, the active agent should merge reusable content into existing durable wiki pages and mark each item `done` or `skipped` without delaying unrelated answers. When pending reaches the lifecycle threshold, `--apply-lifecycle` can promote explicit/high-signal items to `approved`, skip stale low-signal items, and archive old reviewed entries; it does not create durable wiki pages automatically.
242
+ If the queue is always empty during ordinary Q&A, that is normal. If you expected an explicit documentation request or structured durable conclusion to queue, confirm hooks run and that the turn had a captured `UserPromptSubmit`. If pending items stay around, the active agent should batch-review them, merge only explicit, repeated, or clearly reusable content into existing durable wiki pages, and mark each item `done` or `skipped` without delaying unrelated answers. When pending reaches the lifecycle threshold, `--apply-lifecycle` keeps candidates pending for batch review, skips stale low-signal items, and archives old reviewed entries; it does not create durable wiki pages automatically or auto-approve promotion.
243
243
 
244
244
  ## Authentication Values Were Redacted
245
245
 
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.19",
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": [
@@ -123,7 +123,8 @@ export function classifyTurn(entry, eventName = '') {
123
123
  const hasVerification = Boolean(verification);
124
124
  const hasWorkEvidence = hasWork || hasFiles || hasVerification;
125
125
  const durableConclusion = hasDurableConclusion(text);
126
- const durableSignal = durableConclusion || hasDurableKeyword(text);
126
+ const durableKeyword = hasDurableKeyword(text);
127
+ const durableSignal = durableConclusion || durableKeyword;
127
128
  const durableUpdated = hasDetectedDurableWikiChange(entry);
128
129
 
129
130
  if (hasWorkEvidence) {
@@ -135,7 +136,7 @@ export function classifyTurn(entry, eventName = '') {
135
136
  queueIfMissingDurable: false,
136
137
  };
137
138
  }
138
- if (durableSignal) {
139
+ if (durableConclusion) {
139
140
  return {
140
141
  kind: 'suggest-durable',
141
142
  archive: true,
@@ -144,6 +145,14 @@ export function classifyTurn(entry, eventName = '') {
144
145
  signalLevel: durableSignalLevel({ durableConclusion, hasFiles, hasVerification, durableSignal }),
145
146
  };
146
147
  }
148
+ if (durableKeyword) {
149
+ return {
150
+ kind: 'work',
151
+ archive: true,
152
+ suggestDurable: false,
153
+ queueIfMissingDurable: false,
154
+ };
155
+ }
147
156
  return {
148
157
  kind: 'work',
149
158
  archive: true,
@@ -204,7 +204,7 @@ export async function handlePreCompactCapture(projectRoot, provider, eventName,
204
204
  followUp: appendToField(
205
205
  entry.followUp,
206
206
  classification.suggestDurable
207
- ? 'PreCompact saved this checkpoint and queued durable wiki review; merge reusable facts into existing wiki pages after the current answer.'
207
+ ? 'PreCompact saved this checkpoint and queued batch durable review; merge only if it remains reusable with related candidates.'
208
208
  : 'PreCompact saved this checkpoint before context compaction; reusable facts should be merged only when appropriate.'
209
209
  ),
210
210
  };
@@ -220,7 +220,7 @@ export async function handlePreCompactCapture(projectRoot, provider, eventName,
220
220
  const result = await recordMaintenanceForEntry(projectRoot, entry, {
221
221
  source: liveQaPath,
222
222
  eventName,
223
- reason: 'PreCompact durable candidate checkpoint needs review after context compaction.',
223
+ reason: 'PreCompact structured durable checkpoint is waiting for batch review after context compaction.',
224
224
  signalLevel: classification.signalLevel,
225
225
  });
226
226
  if (result.created === false && result.reason) {
@@ -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 || {};
@@ -85,12 +90,12 @@ function durableQueueReason(classification) {
85
90
  if (classification.kind === 'explicit-durable') {
86
91
  return 'Explicit durable documentation request did not have a detected durable wiki update.';
87
92
  }
88
- return 'Suggested durable wiki candidate did not have a detected durable wiki update.';
93
+ return 'Structured durable conclusion is waiting for batch review.';
89
94
  }
90
95
 
91
96
  function durableFollowUp(entry, classification) {
92
97
  if (!classification.suggestDurable) return entry.followUp;
93
- const message = 'This turn is a durable wiki candidate. After the current answer, merge reusable facts into an existing durable wiki page or mark the maintenance item skipped.';
98
+ const message = 'This turn has a structured durable conclusion. Keep it in the maintenance queue for batch review; merge only if it remains reusable with related candidates, otherwise mark it skipped.';
94
99
  const current = String(entry.followUp || '').trim();
95
100
  if (!current || current === '(not captured)') return message;
96
101
  if (current.includes(message)) return current;
@@ -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
 
@@ -11,7 +11,7 @@ export const MAINTENANCE_QUEUE_REL = 'llm-wiki/outputs/maintenance/queue.md';
11
11
  const DEFAULT_STALE_TURN_MS = 10 * 60 * 1000;
12
12
  const DEFAULT_STALE_PENDING_DAYS = 7;
13
13
  const DEFAULT_PENDING_LIMIT = 20;
14
- const DEFAULT_REVIEW_PENDING_LIMIT = 5;
14
+ const DEFAULT_REVIEW_PENDING_LIMIT = 15;
15
15
  const DEFAULT_REVIEW_INTERVAL_DAYS = 14;
16
16
  const DEFAULT_LIFECYCLE_PENDING_THRESHOLD = 15;
17
17
  const DEFAULT_LOW_SIGNAL_SKIP_DAYS = 14;
@@ -27,10 +27,10 @@ function queueHeader() {
27
27
  return [
28
28
  '# LLM Wiki Maintenance Queue',
29
29
  '',
30
- 'Candidates to merge into durable wiki pages. Hooks only create candidates; the active agent reviews and merges them into existing durable wiki documents.',
30
+ 'Candidates for batch durable wiki review. Hooks only create candidates; the active agent merges only explicit, repeated, or clearly reusable facts into existing durable wiki documents.',
31
31
  '',
32
32
  'Status values: pending, approved, done, skipped.',
33
- 'Lifecycle keeps explicit/high-signal candidates visible, may skip old low-signal candidates, and archives old reviewed items.',
33
+ 'Lifecycle keeps candidates pending until batch review, may skip old low-signal candidates, and archives old reviewed items.',
34
34
  '',
35
35
  ].join('\n');
36
36
  }
@@ -70,6 +70,7 @@ function inferSignalLevelFromReason(reason, resultMissing = false) {
70
70
  if (resultMissing || text.includes('recovered from stale turn state')) return 'recovered';
71
71
  if (text.includes('explicit durable documentation request')) return 'explicit';
72
72
  if (text.includes('precompact durable candidate')) return 'high';
73
+ if (text.includes('structured durable conclusion')) return 'high';
73
74
  if (text.includes('suggested durable wiki candidate')) return 'high';
74
75
  if (text.includes('durable wiki review')) return 'high';
75
76
  return 'high';
@@ -293,29 +294,6 @@ export async function applyMaintenanceLifecycle(projectRoot, options = {}) {
293
294
  nextItems = nextItems.map((item) => {
294
295
  if (item.status !== 'pending') return item;
295
296
  const signal = signalLevelForItem(item);
296
- if (signal === 'explicit' || signal === 'high') {
297
- approvedByLifecycle += 1;
298
- actions.push(lifecycleAction('approve', item, { reason: `pending threshold ${defaults.pendingThreshold} reached` }));
299
- const target = item.target || item.suggested_target || '';
300
- const reviewNote = sanitizeField('Auto-approved by lifecycle because the pending queue reached the review threshold.', 500);
301
- return {
302
- ...item,
303
- status: 'approved',
304
- signal_level: signal,
305
- target,
306
- reviewed_at: now,
307
- last_seen_at: now,
308
- review_note: item.review_note || reviewNote,
309
- fields: {
310
- ...(item.fields || {}),
311
- signal_level: signal,
312
- ...(target ? { target } : {}),
313
- reviewed_at: now,
314
- last_seen_at: now,
315
- review_note: item.review_note || reviewNote,
316
- },
317
- };
318
- }
319
297
  if (signal === 'low' && isStaleByDays(item, defaults.lowSignalSkipDays)) {
320
298
  skippedByLifecycle += 1;
321
299
  actions.push(lifecycleAction('skip', item, { reason: `low-signal item older than ${defaults.lowSignalSkipDays} days` }));
@@ -403,7 +381,7 @@ export function summarizeMaintenanceQueue(queue, options = {}) {
403
381
  .filter(Number.isFinite);
404
382
  const lastReviewMs = reviewTimes.length > 0 ? Math.max(...reviewTimes) : null;
405
383
  const reviewReasons = [];
406
- if (approved.length > 0) reviewReasons.push(`approved queue has ${approved.length} item(s) ready for durable wiki merge`);
384
+ if (approved.length > 0) reviewReasons.push(`approved queue has ${approved.length} item(s) ready for manual durable wiki review`);
407
385
  if (pending.length >= reviewPendingLimit) reviewReasons.push(`pending queue has ${pending.length} items (threshold ${reviewPendingLimit})`);
408
386
  if (stalePending.length > 0) reviewReasons.push(`${stalePending.length} pending item(s) older than ${staleDays} days`);
409
387
  if (pending.some((item) => item.result_missing)) reviewReasons.push('pending recovered turn state needs review');
@@ -622,28 +600,30 @@ export async function recoverStaleTurnStates(projectRoot, options = {}) {
622
600
  return output;
623
601
  }
624
602
 
625
- function isDurablePromotionCandidate(item) {
626
- if (item?.status === 'approved') return true;
603
+ function isExplicitDurableCandidate(item) {
604
+ const signal = signalLevelForItem(item);
605
+ if (signal === 'explicit') return true;
627
606
  const text = `${item?.reason || ''} ${item?.fields?.reason || ''}`.toLowerCase();
628
- return /suggested durable wiki candidate|precompact durable candidate|explicit durable documentation|durable wiki review/.test(text);
607
+ return /explicit durable documentation/.test(text);
629
608
  }
630
609
 
631
610
  function shouldSurfacePromptMaintenance(summary, query, reviewCandidates) {
632
611
  if (isMaintenanceRelatedQuery(query || '', reviewCandidates)) return true;
633
612
  if ((summary.approvedCount || 0) > 0) return true;
634
- if (reviewCandidates.some(isDurablePromotionCandidate)) return true;
613
+ if (reviewCandidates.some(isExplicitDurableCandidate)) return true;
635
614
  if ((summary.pendingCount || 0) >= (summary.reviewPendingLimit || DEFAULT_REVIEW_PENDING_LIMIT)) return true;
636
- if ((summary.stalePendingCount || 0) > 0) return true;
637
615
  return reviewCandidates.some((item) => item.result_missing);
638
616
  }
639
617
 
640
618
  export function formatMaintenanceContext(summary, options = {}) {
641
- if (!summary.reviewDue) return '';
642
619
  const language = options.language === 'ko' ? 'ko' : 'en';
643
620
  const eventName = options.eventName || '';
644
621
  const defaultLimit = eventName === 'SessionStart' || eventName === 'InstructionsLoaded' ? 1 : 5;
645
622
  const limit = options.limit || defaultLimit;
646
623
  const reviewCandidates = [...(summary.approved || []), ...(summary.pending || [])];
624
+ const maintenancePrompt = eventName === 'UserPromptSubmit' &&
625
+ isMaintenanceRelatedQuery(options.query || '', reviewCandidates);
626
+ if (!summary.reviewDue && !maintenancePrompt) return '';
647
627
  let pending = reviewCandidates.slice(0, limit);
648
628
 
649
629
  if (eventName === 'UserPromptSubmit') {
@@ -654,16 +634,19 @@ export function formatMaintenanceContext(summary, options = {}) {
654
634
  }
655
635
 
656
636
  const reviewCount = (summary.approvedCount || 0) + (summary.pendingCount || 0);
637
+ const reviewDueText = summary.reviewDue
638
+ ? `yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'})`
639
+ : 'no (maintenance prompt)';
657
640
  const lines = language === 'ko'
658
641
  ? [
659
642
  'LLM Wiki maintenance status:',
660
- `- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
661
- `- approved/pending review items: ${reviewCount}. 현재 요청이 우선이며, 관련 있거나 세션 종료 여유가 있으면 durable wiki에 병합하거나 skipped로 표시한다.`,
643
+ `- review due: ${reviewDueText}.`,
644
+ `- approved/pending review items: ${reviewCount}. 현재 요청이 우선이다. 후보는 충분히 쌓인 batch review에서 관련 항목만 durable wiki에 병합하고, 나머지는 skipped로 표시한다.`,
662
645
  ]
663
646
  : [
664
647
  'LLM Wiki maintenance status:',
665
- `- review due: yes (${(summary.reviewReasons || []).slice(0, 2).join('; ') || 'periodic review threshold met'}).`,
666
- `- approved/pending review items: ${reviewCount}. The current request comes first; when relevant or before ending the session, merge candidates into durable wiki pages or mark them skipped.`,
648
+ `- review due: ${reviewDueText}.`,
649
+ `- approved/pending review items: ${reviewCount}. The current request comes first. Review candidates in batches after enough have accumulated, merge only relevant durable facts, and mark the rest skipped.`,
667
650
  ];
668
651
  for (const item of pending) {
669
652
  const target = item.target || item.suggested_target;
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,16 +9,16 @@ This repository uses llm-wiki-kit as a hook-first living Markdown wiki for Codex
9
9
 
10
10
  - This block supersedes older OMX/OMC/\`omx_wiki/\` LLM Wiki instructions for this repository.
11
11
  - Use Codex or Claude Code normally. Do not create a workflow where users must remember extra \`llm-wiki\` commands during ordinary work.
12
- - Language: respond in the user's current prompt language when it is clearly Korean or English. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
12
+ - Language: respond in the user's current real prompt language when it is clearly Korean or English. Codex synthetic plan execution prompts such as \`Implement the plan.\` are neutral and should reuse the last real session language or project preference. Keep technical identifiers, commands, paths, code, and original error text unchanged. If no prompt language is clear, follow local \`CLAUDE.md\`/\`AGENTS.md\` guidance, then fall back to English.
13
13
  - Treat chat memory as temporary. Durable project knowledge belongs in repository Markdown under \`llm-wiki/\`.
14
14
  - \`llm-wiki/raw/\` is the immutable or redacted evidence layer. Do not modify raw captures except safe hook envelope append.
15
15
  - \`llm-wiki/wiki/\` is the curated knowledge layer managed by the agent. Put decisions, architecture, debugging findings, concepts, procedures, and context there.
16
16
  - Keep \`llm-wiki/wiki/memory.md\` short. Link to important documents instead of copying long explanations.
17
17
  - Use hook-injected context when helpful, but answer the current user request first. Use \`llm-wiki context\`, \`llm-wiki lint\`, and \`llm-wiki consolidate\` only as agent maintenance helpers.
18
18
  - Hooks safely store redacted raw envelopes and work/decision-focused live Q&A. Simple answers, status checks, and keyword-only turns should not be promoted to live Q&A or durable wiki by default.
19
- - Hooks may queue durable cleanup candidates in \`llm-wiki/outputs/maintenance/queue.md\` at stop/start boundaries. Session-start lifecycle may move explicit/high-signal candidates to \`approved\`, skip stale low-signal candidates, and archive old reviewed items, but it never creates durable wiki pages automatically. Treat maintenance as a soft agent-side reminder; merge surfaced candidates when relevant or before ending the session, then mark them \`done\` or \`skipped\`.
19
+ - Hooks may queue durable cleanup candidates in \`llm-wiki/outputs/maintenance/queue.md\` at stop/start boundaries. Lifecycle keeps candidates pending for batch review, skips stale low-signal candidates, and archives old reviewed items, but it never creates durable wiki pages automatically. Treat maintenance as a soft agent-side reminder; merge only explicit, repeated, or clearly reusable facts after enough candidates accumulate, then mark items \`done\` or \`skipped\`.
20
20
  - Before creating new wiki pages, search existing pages and update the right one when possible. Reusable facts should not stay only in chunked \`outputs/questions/\`.
21
- - Store one-off work or decision records in \`llm-wiki/outputs/questions/YYYY-MM-DD/live-qa-001.md\` when needed. Merge reusable knowledge into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` when approved, hook-suggested, or clearly important.
21
+ - Store one-off work or decision records in \`llm-wiki/outputs/questions/YYYY-MM-DD/live-qa-001.md\` when needed. Merge reusable knowledge into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` only when explicitly requested, repeated across related candidates, or clearly important.
22
22
  - Record verification commands, evidence files, and uncertainty. Mark inference explicitly and preserve contradictions in Open Questions or Contradictions.
23
23
  - Never store credentials, tokens, passwords, private keys, or raw \`.env\` contents. Store only redacted summaries when needed.
24
24
 
@@ -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.
@@ -64,10 +65,10 @@ These rules replace older OMX/OMC/\`omx_wiki/\` rules for this project.
64
65
  - Important claims should include at least one of: \`source_ids\`, file paths, or verification commands.
65
66
  - When durable knowledge appears, search existing \`wiki/\` pages before creating a new page.
66
67
  - Do not promote simple answers, status checks, keyword-only replies, or one-off chat into live Q&A, \`wiki/queries\`, or maintenance.
67
- - Hooks may flag work/decision turns as durable wiki candidates when they contain reusable architecture, debugging, policy, procedure, or decision signals. Treat those as review prompts, not automatic page creation.
68
- - Merge reusable knowledge into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` based on importance, hook suggestions, and user consent flow.
69
- - Review \`outputs/maintenance/queue.md\` pending or approved items when surfaced by hook context, related to the current request, or review is due. Merge into existing durable wiki pages, then mark items \`done\` or \`skipped\`.
70
- - Periodic maintenance is agent review, not automatic page creation. Session-start lifecycle may approve explicit/high-signal items, skip stale low-signal items, and archive old reviewed items. Show only short reminders at \`SessionStart\`/\`InstructionsLoaded\`, and compact prompt-time reminders when durable candidates or review thresholds need agent attention.
68
+ - Hooks may flag explicit documentation requests, structured conclusions, and recovered work as durable wiki candidates. Treat those as batch review prompts, not automatic page creation.
69
+ - Merge reusable knowledge into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` only when explicitly requested, repeated across related candidates, or clearly important.
70
+ - Review \`outputs/maintenance/queue.md\` pending or approved items when surfaced by hook context, related to the current request, or review is due. Batch related candidates, merge only durable reusable facts into existing wiki pages, then mark items \`done\` or \`skipped\`.
71
+ - Periodic maintenance is agent review, not automatic page creation. Session-start lifecycle keeps candidates pending for batch review, skips stale low-signal items, and archives old reviewed items. Show only short reminders at \`SessionStart\`/\`InstructionsLoaded\`, and compact prompt-time reminders only for explicit documentation, recovered state, maintenance prompts, or batch review thresholds.
71
72
  - Keep \`wiki/memory.md\` short. Use links to current state and important documents instead of long explanations.
72
73
  - Preserve contradictions in \`Contradictions\` or \`Open Questions\`; do not overwrite them silently.
73
74
  - Do not store credentials, tokens, passwords, private keys, or raw \`.env\` contents in wiki.
@@ -106,7 +107,7 @@ superseded_by: []
106
107
  - query: use hook-injected context when useful, but answer the current request first. Manual commands are diagnostics, not required daily workflow.
107
108
  - lint: agent helper for user-requested or wiki maintenance work. Do not run it every turn.
108
109
  - consolidate: safely refresh generated blocks in \`memory.md\`/\`index.md\`. Do not overwrite handwritten sections or curated document bodies.
109
- - maintenance: \`outputs/maintenance/queue.md\` stores stop/start cleanup candidates. Review surfaced durable candidates without delaying the current user request, then mark them \`done\` or \`skipped\`. Use \`llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run\` when queue hygiene needs inspection.
110
+ - maintenance: \`outputs/maintenance/queue.md\` stores stop/start cleanup candidates. Review surfaced durable candidates in batches without delaying the current user request, merge only durable reusable facts, then mark items \`done\` or \`skipped\`. Use \`llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run\` when queue hygiene needs inspection.
110
111
  `;
111
112
  }
112
113
 
@@ -199,7 +200,7 @@ export function procedure(name) {
199
200
  3. Create or update \`wiki/sources/<slug>.md\` only when a source summary is useful.
200
201
  4. Before creating a new page, look for related concept/entity/decision/architecture/debugging/context pages and update those first.
201
202
  5. Do not create duplicates. Merge facts into existing pages when they already exist.
202
- 6. Do not promote simple answers or one-off chat into durable wiki, but do review hook-suggested durable candidates for reusable facts.
203
+ 6. Do not promote simple answers or one-off chat into durable wiki. Let queued candidates accumulate for batch review unless the user explicitly requested documentation.
203
204
  7. Important claims should record source references, confidence, memory type, importance, and verification status.
204
205
  8. When durable entry points change, update \`wiki/memory.md\` briefly or use \`llm-wiki consolidate\`.
205
206
  9. Add a short entry to \`wiki/log.md\` for meaningful wiki changes.
@@ -212,8 +213,8 @@ export function procedure(name) {
212
213
  4. Inspect raw sources only when exact evidence matters.
213
214
  5. Separate verified facts from inference.
214
215
  6. Use \`llm-wiki context "<query>"\` only when manual inspection is useful. Add \`--include-episodic\` only for historical query/context pages, and \`--include-archived\` only for archived/superseded pages.
215
- 7. Do not promote one-off answers, status checks, or keyword-only replies to durable wiki or live Q&A. Archive only work turns with tool evidence, changed-file evidence, verification, structured decision/debugging conclusions, or hook-suggested durable candidates.
216
- 8. Merge reusable facts into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` based on importance, hook suggestions, and user consent flow.
216
+ 7. Do not promote one-off answers, status checks, or keyword-only replies to durable wiki or live Q&A. Archive work turns with tool evidence, changed-file evidence, verification, structured decision/debugging conclusions, or explicit durable requests.
217
+ 8. Merge reusable facts into \`wiki/architecture/\`, \`wiki/debugging/\`, \`wiki/decisions/\`, \`wiki/concepts/\`, or \`procedures/\` only when explicitly requested, repeated across related candidates, or clearly important.
217
218
  `,
218
219
  'lint.md': `# Lint Procedure
219
220
 
@@ -226,7 +227,7 @@ Periodic maintenance is an agent-side task, not a per-turn user command. When ne
226
227
  1. \`llm-wiki lint --workspace <project>\`
227
228
  2. \`llm-wiki maintenance --workspace <project>\`
228
229
  3. \`llm-wiki maintenance --workspace <project> --apply-lifecycle --dry-run\`
229
- 4. Merge pending, approved, or hook-suggested durable candidates into existing durable pages when appropriate, then mark them \`done\` or \`skipped\`.
230
+ 4. Review pending or approved durable candidates in batches; merge only explicit, repeated, or clearly reusable facts into existing durable pages, then mark items \`done\` or \`skipped\`.
230
231
  5. \`llm-wiki consolidate --workspace <project> --dry-run\`
231
232
  6. Run \`llm-wiki consolidate --workspace <project>\` when the dry run is acceptable.
232
233
 
@@ -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