@zosmaai/pi-llm-wiki 0.11.3 → 0.11.5

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.de.md +8 -0
  3. package/README.es.md +8 -0
  4. package/README.fr.md +8 -0
  5. package/README.hi.md +8 -0
  6. package/README.ja.md +8 -0
  7. package/README.ko.md +8 -0
  8. package/README.md +88 -2
  9. package/README.pt.md +8 -0
  10. package/README.ru.md +8 -0
  11. package/README.zh.md +8 -0
  12. package/assets/wiki-dashboard.png +0 -0
  13. package/commands/wiki-digest.md +28 -0
  14. package/commands/wiki-discover.md +30 -0
  15. package/commands/wiki-ingest.md +37 -0
  16. package/commands/wiki-init.md +30 -0
  17. package/commands/wiki-lint.md +25 -0
  18. package/commands/wiki-query.md +37 -0
  19. package/commands/wiki-record.md +36 -0
  20. package/commands/wiki-req.md +56 -0
  21. package/commands/wiki-retro.md +35 -0
  22. package/commands/wiki-run.md +31 -0
  23. package/commands/wiki-skills.md +26 -0
  24. package/commands/wiki-status.md +16 -0
  25. package/dist/extensions/llm-wiki/lib/dashboard-command.js +86 -0
  26. package/dist/extensions/llm-wiki/lib/dashboard.js +175 -0
  27. package/dist/extensions/llm-wiki/lib/guardrails.js +30 -1
  28. package/dist/extensions/llm-wiki/lib/host.js +117 -0
  29. package/dist/extensions/llm-wiki/lib/ingest-worker.js +2 -1
  30. package/dist/extensions/llm-wiki/lib/knowledge-document.js +20 -2
  31. package/dist/extensions/llm-wiki/lib/knowledge-links.js +6 -3
  32. package/dist/extensions/llm-wiki/lib/metadata.js +1 -1
  33. package/dist/extensions/llm-wiki/lib/observation.js +31 -3
  34. package/dist/extensions/llm-wiki/lib/settings-command.js +377 -0
  35. package/dist/extensions/llm-wiki/lib/task-config.js +145 -43
  36. package/dist/extensions/llm-wiki/lib/utils.js +59 -16
  37. package/docs/api.md +24 -1
  38. package/docs/commands.md +6 -1
  39. package/docs/configuration.md +62 -11
  40. package/docs/superpowers/plans/2026-08-09-qmd-retrieval-phase-1-quality-baseline-and-compatibility.md +1520 -0
  41. package/docs/superpowers/roadmaps/2026-08-09-qmd-retrieval-roadmap.md +448 -0
  42. package/docs/superpowers/specs/2026-08-08-qmd-retrieval-design.md +806 -0
  43. package/extensions/llm-wiki/index.ts +48 -6
  44. package/extensions/llm-wiki/lib/dashboard-command.ts +106 -0
  45. package/extensions/llm-wiki/lib/dashboard.ts +210 -0
  46. package/extensions/llm-wiki/lib/guardrails.ts +26 -1
  47. package/extensions/llm-wiki/lib/host.ts +145 -0
  48. package/extensions/llm-wiki/lib/ingest-worker.ts +4 -0
  49. package/extensions/llm-wiki/lib/knowledge-document.ts +20 -2
  50. package/extensions/llm-wiki/lib/knowledge-links.ts +7 -3
  51. package/extensions/llm-wiki/lib/metadata.ts +1 -1
  52. package/extensions/llm-wiki/lib/observation.ts +37 -4
  53. package/extensions/llm-wiki/lib/settings-command.ts +483 -0
  54. package/extensions/llm-wiki/lib/task-config.ts +208 -46
  55. package/extensions/llm-wiki/lib/utils.ts +55 -14
  56. package/package.json +15 -4
  57. package/prompts/wiki-ingest.md +1 -0
  58. package/prompts/wiki-req.md +1 -0
  59. package/prompts/wiki-retro.md +1 -0
  60. package/skills/llm-wiki/SKILL.md +11 -1
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
2
+ import { dirname } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { detectHost, listGlobalSettingsFiles, listProjectSettingsFiles, resolveGlobalSettingsPath, resolveProjectSettingsPath, } from "./host.js";
4
5
  export const TASK_DEFAULTS = {};
5
6
  /**
6
7
  * Resolve whether user-facing wiki notices are enabled (issue #77). Defaults
@@ -9,6 +10,16 @@ export const TASK_DEFAULTS = {};
9
10
  export function noticesEnabled(config) {
10
11
  return config?.notices !== false;
11
12
  }
13
+ /**
14
+ * Resolve whether the personal vault may serve as this project's ambient
15
+ * vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
16
+ * (see the field docs on {@link TaskConfig.ambientPersonalVault}).
17
+ */
18
+ export function personalVaultIsAmbient(config, host = detectHost()) {
19
+ if (typeof config?.ambientPersonalVault === "boolean")
20
+ return config.ambientPersonalVault;
21
+ return host === "pi";
22
+ }
12
23
  /**
13
24
  * Resolve whether agent-trajectory working-memory is enabled (issue #80).
14
25
  * INVERSE polarity of `noticesEnabled`: defaults to `false`; only an explicit
@@ -64,6 +75,9 @@ function readNamespacedConfig(path) {
64
75
  if (typeof section.notices === "boolean") {
65
76
  out.notices = section.notices;
66
77
  }
78
+ if (typeof section.ambientPersonalVault === "boolean") {
79
+ out.ambientPersonalVault = section.ambientPersonalVault;
80
+ }
67
81
  if (typeof section.trajectories === "boolean") {
68
82
  out.trajectories = section.trajectories;
69
83
  }
@@ -73,6 +87,10 @@ function readNamespacedConfig(path) {
73
87
  if (canonical)
74
88
  out.synthesisLanguage = canonical;
75
89
  }
90
+ const maxTokens = section.synthesisMaxTokens;
91
+ if (typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0) {
92
+ out.synthesisMaxTokens = Math.floor(maxTokens);
93
+ }
76
94
  return out;
77
95
  }
78
96
  catch {
@@ -121,14 +139,19 @@ export function validateSynthesisLanguage(tag) {
121
139
  return canonical[0];
122
140
  }
123
141
  /**
124
- * Read a settings JSON file as a plain object, or `{}` when it is absent or
142
+ * Read a settings file as a plain object, or `{}` when it is absent or
125
143
  * corrupt. Reads directly (no `existsSync` pre-check) so there is no
126
144
  * check-then-use race: a missing file throws ENOENT, which the catch treats
127
145
  * the same as an empty file.
146
+ *
147
+ * `config.yml` / `config.yaml` are parsed as YAML — that is the format oh-my-pi
148
+ * migrates its settings to. Everything else is JSON. JSON is a YAML subset, so
149
+ * the YAML parser also accepts a `.yml` file that actually holds JSON.
128
150
  */
129
151
  function readSettingsObject(path) {
130
152
  try {
131
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
153
+ const text = readFileSync(path, "utf-8");
154
+ const parsed = path.endsWith(".yml") || path.endsWith(".yaml") ? parseYaml(text) : JSON.parse(text);
132
155
  if (parsed && typeof parsed === "object")
133
156
  return parsed;
134
157
  }
@@ -138,64 +161,143 @@ function readSettingsObject(path) {
138
161
  return {};
139
162
  }
140
163
  /**
141
- * Persist (or clear) the wiki background `taskModel` in the PROJECT settings
142
- * file `<cwd>/.pi/settings.json` under the namespaced `llm-wiki` key (issue
143
- * #69). Project settings win over global in `loadTaskConfig`, so this takes
144
- * effect immediately on the next config load. Other top-level keys and other
145
- * `llm-wiki` settings are preserved; passing `undefined` removes the key
146
- * (reverting to the session model).
164
+ * Rewrite the `llm-wiki` section of the global settings file.
147
165
  */
148
- export function persistTaskModel(cwd, model) {
149
- const settingsPath = join(cwd, ".pi", "settings.json");
166
+ function updateGlobalSection(mutate) {
167
+ const settingsPath = resolveGlobalSettingsPath();
150
168
  const raw = readSettingsObject(settingsPath);
151
169
  const existing = raw[SETTINGS_KEY];
152
170
  const section = existing && typeof existing === "object" ? { ...existing } : {};
153
- if (model) {
154
- section.taskModel = { provider: model.provider, id: model.id };
155
- }
156
- else {
157
- // biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
158
- delete section.taskModel;
159
- }
171
+ mutate(section);
160
172
  raw[SETTINGS_KEY] = section;
161
173
  mkdirSync(dirname(settingsPath), { recursive: true });
162
174
  writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
163
175
  }
164
176
  /**
165
- * Persist the agent-trajectory flag in the PROJECT settings file
166
- * `<cwd>/.pi/settings.json` under the namespaced `llm-wiki` key (issue #80).
167
- * Mirrors `persistTaskModel`: project settings win in `loadTaskConfig`, other
168
- * keys are preserved. `true` writes `trajectories: true`; `false` removes the
169
- * key (reverting to the default-off behavior).
177
+ * Rewrite the `llm-wiki` section of the project settings file, preserving every
178
+ * other top-level key and every other setting in the section.
179
+ *
180
+ * The target file is chosen by `resolveProjectSettingsPath` — `.pi/settings.json`
181
+ * or `.omp/settings.json` depending on host and on what already exists — and is
182
+ * always JSON, which both hosts read.
170
183
  */
171
- export function persistTrajectoriesEnabled(cwd, enabled) {
172
- const settingsPath = join(cwd, ".pi", "settings.json");
184
+ function updateProjectSection(cwd, mutate) {
185
+ const settingsPath = resolveProjectSettingsPath(cwd);
173
186
  const raw = readSettingsObject(settingsPath);
174
187
  const existing = raw[SETTINGS_KEY];
175
188
  const section = existing && typeof existing === "object" ? { ...existing } : {};
176
- if (enabled) {
177
- section.trajectories = true;
178
- }
179
- else {
180
- // biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key keeps the JSON clean (default is off)
181
- delete section.trajectories;
182
- }
189
+ mutate(section);
183
190
  raw[SETTINGS_KEY] = section;
184
191
  mkdirSync(dirname(settingsPath), { recursive: true });
185
192
  writeFileSync(settingsPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
186
193
  }
194
+ /**
195
+ * Persist (or clear) the wiki background `taskModel` in the PROJECT settings
196
+ * file under the namespaced `llm-wiki` key (issue #69). Project settings win
197
+ * over global in `loadTaskConfig`, so this takes effect immediately on the next
198
+ * config load. Passing `undefined` removes the key (reverting to the session
199
+ * model).
200
+ */
201
+ export function persistTaskModel(cwd, model) {
202
+ updateProjectSection(cwd, (section) => {
203
+ if (model) {
204
+ section.taskModel = { provider: model.provider, id: model.id };
205
+ }
206
+ else {
207
+ // biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key (vs setting undefined) keeps the JSON clean
208
+ delete section.taskModel;
209
+ }
210
+ });
211
+ }
212
+ /**
213
+ * Persist the agent-trajectory flag in the PROJECT settings file under the
214
+ * namespaced `llm-wiki` key (issue #80). Mirrors `persistTaskModel`: `true`
215
+ * writes `trajectories: true`; `false` removes the key (reverting to the
216
+ * default-off behavior).
217
+ */
218
+ export function persistTrajectoriesEnabled(cwd, enabled) {
219
+ updateProjectSection(cwd, (section) => {
220
+ if (enabled) {
221
+ section.trajectories = true;
222
+ }
223
+ else {
224
+ // biome-ignore lint/performance/noDelete: one-off settings rewrite, not a hot path; removing the key keeps the JSON clean (default is off)
225
+ delete section.trajectories;
226
+ }
227
+ });
228
+ }
229
+ /**
230
+ * Merge the `llm-wiki` section from every settings file both hosts may use,
231
+ * lowest precedence first: built-in defaults, then user-level files, then
232
+ * project-level files. Absent files contribute nothing.
233
+ */
187
234
  export function loadTaskConfig(cwd) {
188
- let globalPath;
189
- try {
190
- globalPath = join(getAgentDir(), "settings.json");
235
+ const config = { ...TASK_DEFAULTS };
236
+ for (const path of listGlobalSettingsFiles()) {
237
+ Object.assign(config, readNamespacedConfig(path));
191
238
  }
192
- catch {
193
- globalPath = "";
239
+ for (const path of listProjectSettingsFiles(cwd)) {
240
+ Object.assign(config, readNamespacedConfig(path));
194
241
  }
195
- const projectPath = join(cwd, ".pi", "settings.json");
196
- return {
197
- ...TASK_DEFAULTS,
198
- ...(globalPath ? readNamespacedConfig(globalPath) : {}),
199
- ...readNamespacedConfig(projectPath),
242
+ return config;
243
+ }
244
+ /**
245
+ * Resolve where each setting is defined: project > global > default.
246
+ */
247
+ /** All known setting keys — needed because TASK_DEFAULTS is {} (zero-config). */
248
+ const KNOWN_KEYS = [
249
+ "taskModel",
250
+ "embeddingProvider",
251
+ "embeddingModel",
252
+ "embeddingBaseUrl",
253
+ "embeddingApiKey",
254
+ "embeddingApiKeyEnv",
255
+ "semanticWeight",
256
+ "recallLinksThreshold",
257
+ "recallSkillInlineMax",
258
+ "notices",
259
+ "ambientPersonalVault",
260
+ "trajectories",
261
+ "synthesisLanguage",
262
+ "synthesisMaxTokens",
263
+ ];
264
+ export function loadTaskConfigSources(cwd) {
265
+ const globalResult = {};
266
+ for (const path of listGlobalSettingsFiles()) {
267
+ Object.assign(globalResult, readNamespacedConfig(path));
268
+ }
269
+ const projectResult = {};
270
+ for (const path of listProjectSettingsFiles(cwd)) {
271
+ Object.assign(projectResult, readNamespacedConfig(path));
272
+ }
273
+ const effective = loadTaskConfig(cwd);
274
+ const out = {};
275
+ for (const key of KNOWN_KEYS) {
276
+ if (key in projectResult)
277
+ out[key] = { value: projectResult[key], source: "project" };
278
+ else if (key in globalResult)
279
+ out[key] = { value: globalResult[key], source: "global" };
280
+ else
281
+ out[key] = { value: effective[key], source: "default" };
282
+ }
283
+ return out;
284
+ }
285
+ /**
286
+ * Generic setting persist: writes any single setting to the chosen scope.
287
+ */
288
+ export function persistSetting(cwd, scope, key, value) {
289
+ const mutate = (section) => {
290
+ if (value === undefined || value === null) {
291
+ delete section[key];
292
+ }
293
+ else {
294
+ section[key] = value;
295
+ }
200
296
  };
297
+ if (scope === "project") {
298
+ updateProjectSection(cwd, mutate);
299
+ }
300
+ else if (scope === "global") {
301
+ updateGlobalSection(mutate);
302
+ }
201
303
  }
@@ -82,20 +82,46 @@ export function migrateDoubledPersonalVault(parentRoot = getPersonalWikiRoot())
82
82
  /**
83
83
  * Check if a vault is the personal wiki location.
84
84
  * Used in layered recall to avoid double-counting.
85
+ *
86
+ * Compares PHYSICAL paths, not strings. On image-based ("atomic") Linux
87
+ * distributions `/home` is a symlink to `var/home`, so `homedir()` yields the
88
+ * `$HOME` string (`/home/u`) while `process.cwd()` — and therefore the root
89
+ * `resolveVaultRoot()` walks up to — yields `/var/home/u`. A string compare
90
+ * calls the personal vault a project vault, which makes layered recall search
91
+ * the same vault twice and `vaultPageCount()` double-count it.
92
+ *
93
+ * Exact equality, NOT containment: a vault nested under the home directory
94
+ * (`~/projects/foo/.llm-wiki`) is a project vault and must stay one.
85
95
  */
86
96
  export function isPersonalVault(paths) {
87
- return paths.root === getPersonalWikiRoot();
97
+ const personalRoot = getPersonalWikiRoot();
98
+ // Fast path: identical strings need no filesystem syscalls.
99
+ if (paths.root === personalRoot)
100
+ return true;
101
+ try {
102
+ return relativePhysicalPath(personalRoot, paths.root) === "";
103
+ }
104
+ catch {
105
+ // Unresolvable path (permissions, symlink cycle): fall back to "not
106
+ // personal" so layered recall degrades to searching both vaults rather
107
+ // than silently dropping the personal layer.
108
+ return false;
109
+ }
88
110
  }
89
111
  /**
90
- * Resolve vault root from cwd with personal fallback.
112
+ * Resolve the vault root that belongs to THIS project, or `null` when the
113
+ * project has none.
91
114
  *
92
115
  * Priority:
93
- * 1. cwd has .llm-wiki/ → project wiki (explicit)
94
- * 2. Walk up from cwd → parent project wiki
95
- * 3. ~/.llm-wiki/ exists → personal wiki
96
- * 4. Fallback: ~/.llm-wiki/ (create personal wiki)
116
+ * 1. cwd has `.llm-wiki/` (or legacy `.wiki/`) → project wiki (explicit)
117
+ * 2. `WIKI_HOME` → user-selected root, explicit enough to count as the project's
118
+ * 3. Walk up from cwd → parent project wiki (monorepo / nested workspace)
119
+ *
120
+ * Deliberately does NOT fall back to the personal wiki: callers that need the
121
+ * fallback use {@link resolveVaultRoot}, callers that must distinguish "this
122
+ * project has a wiki" from "some wiki exists somewhere" use this.
97
123
  */
98
- export function resolveVaultRoot(cwd) {
124
+ export function resolveProjectVaultRoot(cwd) {
99
125
  // A vault rooted at cwd is always the project-local choice.
100
126
  if (detectVaultFormat(cwd) !== "none")
101
127
  return cwd;
@@ -103,19 +129,36 @@ export function resolveVaultRoot(cwd) {
103
129
  // over an unrelated personal vault found while walking parent directories.
104
130
  if (process.env.WIKI_HOME)
105
131
  return process.env.WIKI_HOME;
106
- // Walk up looking for a vault sentinel (new or legacy)
132
+ // Walk up looking for a vault sentinel (new or legacy).
107
133
  let dir = cwd;
108
134
  while (dir !== dirname(dir)) {
109
135
  dir = dirname(dir);
110
- if (detectVaultFormat(dir) !== "none")
111
- return dir;
136
+ if (detectVaultFormat(dir) === "none")
137
+ continue;
138
+ // Skip the personal vault: it is an ancestor of EVERY project under the
139
+ // home directory (`~/projects/foo`, and on Windows even the temp dir), so
140
+ // counting it here would report a project vault for directories that have
141
+ // none. `resolveVaultRoot` still falls back to it explicitly.
142
+ if (isPersonalVault(getVaultPaths(dir)))
143
+ continue;
144
+ return dir;
112
145
  }
113
- // Check personal wiki at ~/.llm-wiki/
114
- const personalRoot = getPersonalWikiRoot();
115
- if (detectVaultFormat(personalRoot) !== "none")
116
- return personalRoot;
117
- // Fallback: personal wiki
118
- return personalRoot;
146
+ return null;
147
+ }
148
+ /**
149
+ * Resolve vault root from cwd with personal fallback.
150
+ *
151
+ * Priority:
152
+ * 1-3. {@link resolveProjectVaultRoot}
153
+ * 4. Personal wiki root (`~`, or `WIKI_HOME`) — used whether or not it already
154
+ * holds a vault, so first-run bootstrap has somewhere to write.
155
+ */
156
+ export function resolveVaultRoot(cwd) {
157
+ // Realpath the personal fallback so a symlinked `$HOME` (atomic-OS layouts)
158
+ // yields the PHYSICAL root the ancestor walk used to return — the #145
159
+ // regression guard pins that. `realpathWithMissingTail` also covers first-run
160
+ // bootstrap, where the personal root does not exist on disk yet.
161
+ return resolveProjectVaultRoot(cwd) ?? realpathWithMissingTail(getPersonalWikiRoot());
119
162
  }
120
163
  /** Get all vault paths for the new (.llm-wiki) layout. */
121
164
  export function getVaultPaths(root) {
package/docs/api.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  All tools registered by the extension. Parameters marked `?` are optional.
4
4
 
5
- 13 tools are always registered. The 3 agent-trajectory tools
5
+ 14 tools are always registered. The 3 agent-trajectory tools
6
6
  (`wiki_capture_trajectory`, `wiki_distill_skills`, `wiki_recall_skill`) are **opt-in,
7
7
  off by default** (issue #80) — they are only registered when `llm-wiki.trajectories`
8
8
  is `true`; enable with `/wiki-trajectories on`.
@@ -458,6 +458,29 @@ Returns empty `matches: []` with a hint to capture work via `wiki_capture_trajec
458
458
 
459
459
  ---
460
460
 
461
+ ## wiki_reindex_embeddings
462
+
463
+ Backfill or refresh semantic embeddings for the vault. Embeds pages that are new or stale
464
+ (content changed); pass `force` to re-embed everything. No-op when no embedding provider is
465
+ configured — set `llm-wiki.embeddingProvider` first (see `docs/configuration.md`).
466
+
467
+ **Parameters**
468
+
469
+ | Name | Type | Required | Description |
470
+ |------|------|----------|-------------|
471
+ | `force` | `boolean` | — | Re-embed every page, ignoring staleness (default: `false`) |
472
+
473
+ **Returns**
474
+
475
+ ```
476
+ details: { enabled: true, model: string, embedded: number, skipped: number, pruned: number }
477
+ ```
478
+
479
+ Fails soft with `details: { enabled: false }` plus a hint to configure `embeddingProvider` when
480
+ no embedding provider is set.
481
+
482
+ ---
483
+
461
484
  ## Error Shape
462
485
 
463
486
  All tools return `isError: true` in their result when a hard error occurs (no vault found, missing
package/docs/commands.md CHANGED
@@ -17,10 +17,13 @@
17
17
  | `/wiki-trajectories` | Enable/disable agent working-memory (`on`/`off`, opt-in) |
18
18
  | `/wiki-record` | Capture the completed task's trajectory (requires trajectories enabled) |
19
19
  | `/wiki-skills` | Search distilled skills + past cases (requires trajectories enabled) |
20
+ | `/wiki-req` | Decompose a concept into atomic requirement pages |
21
+ | `/wiki-settings` | Browse or change all `llm-wiki` settings (project or global scope) |
22
+ | `/wiki-dashboard` | Read-only vault health: pages, freshness, activity, ingest queue, backlinks, embeddings |
20
23
 
21
24
  ## Extension Tools
22
25
 
23
- The extension always registers 13 tools the LLM can call directly. The 3 agent-trajectory
26
+ The extension always registers 14 tools the LLM can call directly. The 3 agent-trajectory
24
27
  tools (`wiki_capture_trajectory`, `wiki_distill_skills`, `wiki_recall_skill`) are **opt-in,
25
28
  off by default** (issue #80) — registered only when `llm-wiki.trajectories` is enabled
26
29
  (`/wiki-trajectories on`).
@@ -36,7 +39,9 @@ off by default** (issue #80) — registered only when `llm-wiki.trajectories` is
36
39
  | `wiki_search` | Search the wiki registry |
37
40
  | `wiki_lint` | Health check with auto-fix |
38
41
  | `wiki_status` | Instant stats |
42
+ | `wiki_observe` | Record a timestamped observation from the current session |
39
43
  | `wiki_rebuild_meta` | Force metadata rebuild |
44
+ | `wiki_reindex_embeddings` | Refresh semantic embeddings (no-op when no embedding provider) |
40
45
  | `wiki_log_event` | Record custom event |
41
46
  | `wiki_watch` | Schedule auto-updates |
42
47
  | `wiki_capture_trajectory` | Capture the completed task's tool-call trajectory |
@@ -31,17 +31,44 @@ The personal vault lives at `~/.llm-wiki/` (or `$WIKI_HOME`) and is always avail
31
31
  | ----------------------------- | ----------- | ----------------------------------------------- |
32
32
  | `WIKI_HOME` | `~/.llm-wiki` | Override the personal wiki vault location |
33
33
  | `WIKI_MARKITDOWN_TIMEOUT_MS` | 180000 | Timeout (ms) for MarkItDown PDF/text extraction |
34
-
35
- ## Pi Agent Settings
36
-
37
- Runtime settings for the wiki's background tasks live in `.pi/settings.json` under the `llm-wiki` namespace. These can be set globally (`~/.pi/agent/settings.json`) or per-project (`<cwd>/.pi/settings.json`).
38
-
39
- | Setting | Default | Description |
40
- | --------------------- | ------- | ------------------------------------------------------------ |
41
- | `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
42
- | `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
43
- | `trajectories` | false | Enable agent-trajectory working-memory |
44
- | `notices` | true | Show wiki activity notices in chat |
34
+ | `LLM_WIKI_HOST` | auto | Force the host layout: `pi` or `omp` |
35
+
36
+ ## Agent Settings
37
+
38
+ Runtime settings for the wiki's background tasks live under the `llm-wiki`
39
+ namespace of the host's settings file. Both host layouts are read and merged,
40
+ lowest precedence first:
41
+
42
+ 1. `<agentDir>/settings.json`, then `config.yml` / `config.yaml`
43
+ — `~/.pi/agent` under pi, `~/.omp/agent` under oh-my-pi
44
+ 2. `<cwd>/.pi/{settings.json,config.yml,config.yaml}`
45
+ 3. `<cwd>/.omp/{settings.json,config.yml,config.yaml}`
46
+
47
+ The **host-native** project directory is applied last, so it wins: `.omp` under
48
+ oh-my-pi, `.pi` under pi. Reading the other host's directory means a vault
49
+ configured under pi keeps working after `omp` takes over the repository.
50
+
51
+ `/wiki-model` and `/wiki-trajectories` write JSON only, into whichever project
52
+ config directory already exists (host-native first, created if neither is
53
+ present). A hand-authored `config.yml` is read but never rewritten.
54
+
55
+ | Setting | Default | Description |
56
+ All of the above are viewable and editable in the `/wiki-settings` TUI (persists to project or global settings).
57
+ | ---------------------- | ---------- | ------------------------------------------------------------ |
58
+ | `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
59
+ | `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
60
+ | `synthesisMaxTokens` | 16384 | Max output tokens for ingest/synthesis runs (stored as a plain number) |
61
+ | `trajectories` | false | Enable agent-trajectory working-memory |
62
+ | `notices` | true | Show wiki activity notices in chat |
63
+ | `ambientPersonalVault` | host-dependent | Let the personal vault act as the ambient vault in projects that have no wiki. `true` under pi, `false` under oh-my-pi — see below. |
64
+ | `semanticWeight` | 0.5 | Weight of the semantic sub-score in hybrid recall (clamped 0–1) |
65
+ | `recallLinksThreshold` | 50 | Page-count gate for two-stage links-first recall (0 = always links-first, issue #68) |
66
+ | `recallSkillInlineMax` | 1600 | Max chars of a skill/case body inlined into recall output (0 = links only) |
67
+ | `embeddingProvider` | — | Embedding provider (e.g. `openai`); embeddings stay off until this is set |
68
+ | `embeddingModel` | — | Embedding model name (provider-specific) |
69
+ | `embeddingBaseUrl` | — | Optional API base URL override for the embedding provider |
70
+ | `embeddingApiKey` | — | API key literal — prefer `embeddingApiKeyEnv` so no secret lands in settings |
71
+ | `embeddingApiKeyEnv` | — | Name of the environment variable holding the embedding API key |
45
72
 
46
73
  Example:
47
74
 
@@ -66,6 +93,30 @@ The vault root is resolved in this priority order:
66
93
 
67
94
  This means when you're in a project with its own `.llm-wiki/`, that project wiki is active. When you're outside any project wiki, your personal `~/.llm-wiki/` takes over automatically.
68
95
 
96
+ ### Ambient surfaces in projects without a wiki
97
+
98
+ Three surfaces fire without being asked: the session notice, the periodic
99
+ observe/retro reminder, and the `before_agent_start` recall injection (plus its
100
+ `<wiki_status>` system-prompt footer).
101
+
102
+ Because vault resolution falls back to the personal vault, those surfaces would
103
+ otherwise speak up in *every* directory as soon as `~/.llm-wiki/` exists —
104
+ injecting reminders and unrelated cross-project recall hits into repositories
105
+ where no wiki was ever initialized. Under oh-my-pi the plugin is installed once
106
+ and loads in every project, so that fallback is **off** by default there; under
107
+ pi the historical behaviour is kept.
108
+
109
+ `ambientPersonalVault` overrides the host default in either direction:
110
+
111
+ ```json
112
+ { "llm-wiki": { "ambientPersonalVault": true } }
113
+ ```
114
+
115
+ The gate only affects unprompted injections. Tools and slash commands are
116
+ always registered, so `/wiki-init` and `wiki_bootstrap` work in any directory —
117
+ and once a project has its own `.llm-wiki/`, every ambient surface turns back on
118
+ for it.
119
+
69
120
  ## Page Frontmatter
70
121
 
71
122
  ```yaml