@zosmaai/pi-llm-wiki 0.11.2 → 0.11.4

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 (43) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.de.md +2 -1
  3. package/README.es.md +2 -1
  4. package/README.fr.md +2 -1
  5. package/README.hi.md +2 -1
  6. package/README.ja.md +2 -1
  7. package/README.ko.md +2 -1
  8. package/README.md +89 -10
  9. package/README.pt.md +2 -1
  10. package/README.ru.md +2 -1
  11. package/README.zh.md +2 -1
  12. package/commands/wiki-digest.md +28 -0
  13. package/commands/wiki-discover.md +30 -0
  14. package/commands/wiki-ingest.md +36 -0
  15. package/commands/wiki-init.md +30 -0
  16. package/commands/wiki-lint.md +25 -0
  17. package/commands/wiki-query.md +37 -0
  18. package/commands/wiki-record.md +36 -0
  19. package/commands/wiki-req.md +55 -0
  20. package/commands/wiki-retro.md +34 -0
  21. package/commands/wiki-run.md +31 -0
  22. package/commands/wiki-skills.md +26 -0
  23. package/commands/wiki-status.md +16 -0
  24. package/dist/extensions/llm-wiki/lib/host.js +97 -0
  25. package/dist/extensions/llm-wiki/lib/observation.js +9 -0
  26. package/dist/extensions/llm-wiki/lib/runtime.js +13 -1
  27. package/dist/extensions/llm-wiki/lib/task-config.js +74 -49
  28. package/dist/extensions/llm-wiki/lib/tools.js +10 -7
  29. package/dist/extensions/llm-wiki/lib/utils.js +59 -16
  30. package/dist/mcp/index.js +55 -3
  31. package/dist/mcp/operations.js +39 -3
  32. package/docs/api.md +9 -1
  33. package/docs/configuration.md +49 -8
  34. package/extensions/llm-wiki/index.ts +44 -6
  35. package/extensions/llm-wiki/lib/host.ts +125 -0
  36. package/extensions/llm-wiki/lib/observation.ts +14 -1
  37. package/extensions/llm-wiki/lib/runtime.ts +13 -1
  38. package/extensions/llm-wiki/lib/task-config.ts +115 -55
  39. package/extensions/llm-wiki/lib/tools.ts +10 -7
  40. package/extensions/llm-wiki/lib/utils.ts +55 -14
  41. package/mcp/index.ts +65 -2
  42. package/mcp/operations.ts +47 -4
  43. package/package.json +12 -1
@@ -5,8 +5,9 @@
5
5
  * used by Pi tools. No operation parses YAML, scans files, scores
6
6
  * registry entries, or builds page strings itself.
7
7
  */
8
+ import { bootstrapVault } from "../extensions/llm-wiki/lib/bootstrap.js";
8
9
  import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
9
- import { searchWiki } from "../extensions/llm-wiki/lib/recall.js";
10
+ import { searchWikiLayered } from "../extensions/llm-wiki/lib/recall.js";
10
11
  import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
11
12
  import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
12
13
  import { VaultWriteError, inspectVaultFormat, inspectWritableVault, } from "../extensions/llm-wiki/lib/vault-format.js";
@@ -19,9 +20,44 @@ function projectionOutcome(projection) {
19
20
  diagnostics: projection.diagnostics.map(({ code, message }) => ({ code, message })),
20
21
  };
21
22
  }
22
- /** Shared recall operation: calls searchWiki and appends vault diagnostics. */
23
+ /**
24
+ * Shared bootstrap operation: create (or update) the vault at `paths`.
25
+ *
26
+ * This is the one operation that must work when no vault exists — every other
27
+ * one fails closed naming it. `bootstrapVault` is pure Node (`node:fs`,
28
+ * `node:path` and sibling lib modules), so it needs no model and no
29
+ * credentials, which is what makes it fit the MCP surface.
30
+ *
31
+ * A failed projection rebuild is reported as diagnostics alongside `ok: true`:
32
+ * the vault has been written to disk by then, and `wiki_lint` is the repair
33
+ * path, so failing the call outright would misreport what happened.
34
+ */
35
+ export async function bootstrapOperation(paths, input) {
36
+ const result = bootstrapVault(paths, { topic: input.topic, mode: input.mode ?? "personal" });
37
+ if (!result.ok) {
38
+ return {
39
+ ok: false,
40
+ diagnostics: result.diagnostics.map(({ code, message }) => ({ code, message })),
41
+ };
42
+ }
43
+ const projection = projectionOutcome(result.projection);
44
+ return {
45
+ ok: true,
46
+ created: result.created,
47
+ diagnostics: projection.ok ? [] : projection.diagnostics,
48
+ };
49
+ }
50
+ /**
51
+ * Shared recall operation: layered search plus vault diagnostics.
52
+ *
53
+ * Layering is the shared contract, not an extension-only feature: MCP clients
54
+ * get the same personal + project merge the Pi `wiki_recall` tool does.
55
+ * `searchWikiLayered` appends personal-vault hits, deduplicates by page ID and
56
+ * tags personal results with `vaultLabel`. It is a no-op when no personal vault
57
+ * exists, or when the resolved vault IS the personal vault.
58
+ */
23
59
  export async function recallOperation(paths, query, maxResults = 5) {
24
- const results = searchWiki(paths, query, maxResults);
60
+ const results = searchWikiLayered(paths, query, maxResults);
25
61
  const vaultState = inspectVaultFormat(paths);
26
62
  return {
27
63
  results,
package/docs/api.md CHANGED
@@ -223,6 +223,10 @@ Deterministic health check of the wiki. Scans for orphan pages (no inbound links
223
223
  (linked but not created), and contradiction markers. Optionally auto-creates stub pages for
224
224
  knowledge gaps cited in two or more pages.
225
225
 
226
+ Runs **asynchronously**: the tool acknowledges immediately and scans off-thread. On completion a
227
+ UI toast fires instantly (when a UI is available) and the full health report is delivered with
228
+ the next user message.
229
+
226
230
  **Parameters**
227
231
 
228
232
  | Name | Type | Required | Description |
@@ -277,9 +281,13 @@ Health is `"⚠️ Warning"` when orphan count exceeds 5, `"🔴 Empty"` when th
277
281
 
278
282
  ## wiki_rebuild_meta
279
283
 
280
- Force a full synchronous rebuild of all generated metadata: `registry.json`, `backlinks.json`,
284
+ Force a full rebuild of all generated metadata: `registry.json`, `backlinks.json`,
281
285
  `index.md`, `log.md`. Use when metadata appears out of sync with actual wiki files.
282
286
 
287
+ Runs **asynchronously**: the tool acknowledges immediately and rebuilds off-thread. On completion
288
+ a UI toast fires instantly (when a UI is available) and the result is delivered with the next
289
+ user message.
290
+
283
291
  If `meta/events.jsonl` is missing or unreadable, rebuild reports a warning and preserves existing log projections while continuing to rebuild registry, backlinks, and indexes. A present zero-byte event file is an intentional empty history.
284
292
 
285
293
  **Parameters**
@@ -31,17 +31,34 @@ 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
+ | `LLM_WIKI_HOST` | auto | Force the host layout: `pi` or `omp` |
34
35
 
35
- ## Pi Agent Settings
36
+ ## Agent Settings
36
37
 
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
+ 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:
38
41
 
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 |
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
+ | ---------------------- | ---------- | ------------------------------------------------------------ |
57
+ | `taskModel` | — | Model for background tasks (`{ provider: "openai", id: "gpt-4o" }`) |
58
+ | `synthesisLanguage` | — | BCP 47 language tag for ingest synthesis (e.g. `"ru"`, `"fr"`). When unset, synthesis defaults to English. |
59
+ | `trajectories` | false | Enable agent-trajectory working-memory |
60
+ | `notices` | true | Show wiki activity notices in chat |
61
+ | `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. |
45
62
 
46
63
  Example:
47
64
 
@@ -66,6 +83,30 @@ The vault root is resolved in this priority order:
66
83
 
67
84
  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
85
 
86
+ ### Ambient surfaces in projects without a wiki
87
+
88
+ Three surfaces fire without being asked: the session notice, the periodic
89
+ observe/retro reminder, and the `before_agent_start` recall injection (plus its
90
+ `<wiki_status>` system-prompt footer).
91
+
92
+ Because vault resolution falls back to the personal vault, those surfaces would
93
+ otherwise speak up in *every* directory as soon as `~/.llm-wiki/` exists —
94
+ injecting reminders and unrelated cross-project recall hits into repositories
95
+ where no wiki was ever initialized. Under oh-my-pi the plugin is installed once
96
+ and loads in every project, so that fallback is **off** by default there; under
97
+ pi the historical behaviour is kept.
98
+
99
+ `ambientPersonalVault` overrides the host default in either direction:
100
+
101
+ ```json
102
+ { "llm-wiki": { "ambientPersonalVault": true } }
103
+ ```
104
+
105
+ The gate only affects unprompted injections. Tools and slash commands are
106
+ always registered, so `/wiki-init` and `wiki_bootstrap` work in any directory —
107
+ and once a project has its own `.llm-wiki/`, every ambient surface turns back on
108
+ for it.
109
+
69
110
  ## Page Frontmatter
70
111
 
71
112
  ```yaml
@@ -20,7 +20,12 @@ import {
20
20
  } from "./lib/recall.js";
21
21
  import { registerWikiRetro } from "./lib/retro.js";
22
22
  import { registerBackgroundRuntime } from "./lib/runtime.js";
23
- import { loadTaskConfig, noticesEnabled, trajectoriesEnabled } from "./lib/task-config.js";
23
+ import {
24
+ loadTaskConfig,
25
+ noticesEnabled,
26
+ personalVaultIsAmbient,
27
+ trajectoriesEnabled,
28
+ } from "./lib/task-config.js";
24
29
  import {
25
30
  registerWikiBootstrap,
26
31
  registerWikiCaptureSource,
@@ -40,7 +45,11 @@ import {
40
45
  registerWikiDistillSkills,
41
46
  registerWikiRecallSkill,
42
47
  } from "./lib/trajectory.js";
43
- import { migrateDoubledPersonalVault, resolveVaultPaths } from "./lib/utils.js";
48
+ import {
49
+ migrateDoubledPersonalVault,
50
+ resolveProjectVaultRoot,
51
+ resolveVaultPaths,
52
+ } from "./lib/utils.js";
44
53
  import { inspectWritableVault } from "./lib/vault-format.js";
45
54
  import { applySessionStartStatus } from "./lib/visible-status.js";
46
55
 
@@ -68,6 +77,26 @@ export default function (pi: ExtensionAPI) {
68
77
  // work. Created first so tools (e.g. wiki_ingest) can dispatch to it.
69
78
  const runtime = registerBackgroundRuntime(pi);
70
79
 
80
+ /**
81
+ * Does a wiki apply to the directory this session is working in?
82
+ *
83
+ * Gates every AMBIENT surface — the ones that speak without being asked:
84
+ * session bootstrap/notice, the periodic observe/retro reminder, and
85
+ * `before_agent_start` recall injection. Tools and commands are registered
86
+ * regardless, so `/wiki-init` remains the way in.
87
+ *
88
+ * `resolveVaultRoot` falls back to the personal vault when a project has
89
+ * none, which is why the ambient surfaces used to fire in EVERY directory
90
+ * once a personal vault existed — reminders and unrelated cross-project
91
+ * recall hits leaking into repositories that never initialized a wiki.
92
+ * Under omp that fallback is off by default (`llm-wiki.ambientPersonalVault`);
93
+ * under pi it stays on, preserving the historical behavior.
94
+ *
95
+ * Resolved per call, not once at load: `cwd` changes within a session.
96
+ */
97
+ const wikiAppliesTo = (cwd: string): boolean =>
98
+ resolveProjectVaultRoot(cwd) !== null || personalVaultIsAmbient(runtime.config);
99
+
71
100
  registerWikiBootstrap(pi);
72
101
  registerWikiCaptureSource(pi, runtime);
73
102
  registerWikiIngest(pi, runtime);
@@ -106,9 +135,11 @@ export default function (pi: ExtensionAPI) {
106
135
  registerWikiObserve(pi, runtime, reminderState);
107
136
  // Visible observe/retro reminder by default (issue #77); silenced when the
108
137
  // user sets `llm-wiki.notices: false`. Resolver reads the live config so the
109
- // setting takes effect without a restart.
138
+ // setting takes effect without a restart. `display: false` still injects the
139
+ // reminder into model context, so the "no wiki here" case needs its own gate.
110
140
  registerObservationReminder(pi, reminderState, {
111
141
  display: () => noticesEnabled(runtime.config),
142
+ enabled: () => wikiAppliesTo(process.cwd()),
112
143
  });
113
144
 
114
145
  installGuardrails(pi, runtime);
@@ -137,6 +168,12 @@ export default function (pi: ExtensionAPI) {
137
168
  console.warn(`[llm-wiki] doubled-dotdir migration skipped: ${(err as Error).message}`);
138
169
  }
139
170
 
171
+ // Ambient gate. `ensureConfig` first so an explicit
172
+ // `llm-wiki.ambientPersonalVault` is honored on the very first session —
173
+ // `runtime.config` is otherwise empty until the first `turn_start`.
174
+ runtime.ensureConfig(process.cwd());
175
+ if (!wikiAppliesTo(process.cwd())) return;
176
+
140
177
  const paths = resolveVaultPaths(process.cwd());
141
178
  if (!existsSync(join(paths.dotWiki, "config.json"))) {
142
179
  // Silently create the wiki vault — no UI prompts. Topic/mode will be
@@ -168,9 +205,8 @@ export default function (pi: ExtensionAPI) {
168
205
 
169
206
  // Surface the "wiki active" badge and the active background task model
170
207
  // (issue #69), both gated by `llm-wiki.notices` (issue #77, regression
171
- // fixed in #83, helper extracted in #84). `ensureConfig` MUST run first so
172
- // the gate sees the loaded project settings.
173
- runtime.ensureConfig(process.cwd());
208
+ // fixed in #83, helper extracted in #84). The `ensureConfig` above the
209
+ // ambient gate already loaded the project settings this reads.
174
210
  applySessionStartStatus({
175
211
  ui: ctx.ui,
176
212
  runtime,
@@ -196,6 +232,8 @@ export default function (pi: ExtensionAPI) {
196
232
  // from the user's first prompt and update config via wiki_bootstrap.
197
233
  // 2. Search both personal + project vaults for relevant pages.
198
234
  pi.on("before_agent_start", async (event, ctx) => {
235
+ if (!wikiAppliesTo(process.cwd())) return;
236
+
199
237
  const paths = resolveVaultPaths(process.cwd());
200
238
  if (!existsSync(join(paths.dotWiki, "config.json"))) {
201
239
  return;
@@ -0,0 +1,125 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
4
+
5
+ /**
6
+ * Host adapter for the two coding agents that can load this extension:
7
+ *
8
+ * - **pi** — `@mariozechner/pi-coding-agent`, config dir `.pi`
9
+ * - **omp** — oh-my-pi (`@oh-my-pi/pi-coding-agent`), config dir `.omp`
10
+ *
11
+ * omp rewrites `@mariozechner/pi-*` (and bare `typebox`) imports onto its own
12
+ * bundled packages at load time (its `legacy-pi-compat.ts`), so the *module
13
+ * graph* needs no changes. What does differ is the on-disk config layout:
14
+ *
15
+ * | | pi | omp |
16
+ * |---|---|---|
17
+ * | user dir | `~/.pi/agent` | `~/.omp/agent` |
18
+ * | project dir | `<cwd>/.pi` | `<cwd>/.omp` |
19
+ * | settings file | `settings.json` | `settings.json`, then `config.yml` |
20
+ *
21
+ * omp explicitly does **not** read `.pi` (its config source order is
22
+ * `.omp` → `.claude` → `.codex` → `.gemini`), so a wiki configured under pi
23
+ * would silently lose its settings after switching hosts. This module keeps
24
+ * both layouts readable and picks a sensible file to write to.
25
+ *
26
+ * Everything here is additive: on pi with only a `.pi/` directory the effective
27
+ * behaviour is identical to the pre-compat code path, which keeps upstream
28
+ * merges clean.
29
+ */
30
+
31
+ export type HostKind = "pi" | "omp";
32
+
33
+ /** Project config directory name per host. */
34
+ const CONFIG_DIR: Record<HostKind, string> = { pi: ".pi", omp: ".omp" };
35
+
36
+ /** Settings file names inside a config directory, lowest → highest precedence. */
37
+ const SETTINGS_FILES = ["settings.json", "config.yml", "config.yaml"] as const;
38
+
39
+ /**
40
+ * Detect which agent is hosting this extension.
41
+ *
42
+ * Ordered by reliability:
43
+ * 1. `LLM_WIKI_HOST` — explicit escape hatch (tests, exotic embeddings).
44
+ * 2. The agent directory path: pi resolves `~/.pi/agent`, omp `~/.omp/agent`.
45
+ * A `PI_CODING_AGENT_DIR` override that keeps the marker segment still
46
+ * classifies correctly; anything else falls through.
47
+ * 3. `OMP_PROFILE`, which omp sets on itself whenever a profile is active.
48
+ * 4. Default `pi` — the historical behaviour.
49
+ */
50
+ export function detectHost(): HostKind {
51
+ const forced = process.env.LLM_WIKI_HOST?.trim().toLowerCase();
52
+ if (forced === "omp" || forced === "pi") return forced;
53
+
54
+ let agentDir = "";
55
+ try {
56
+ agentDir = getAgentDir();
57
+ } catch {
58
+ agentDir = "";
59
+ }
60
+ if (agentDir) {
61
+ const segments = agentDir.split(/[\\/]/);
62
+ if (segments.includes(".omp")) return "omp";
63
+ if (segments.includes(".pi")) return "pi";
64
+ }
65
+
66
+ if (process.env.OMP_PROFILE !== undefined) return "omp";
67
+ return "pi";
68
+ }
69
+
70
+ /**
71
+ * Every project settings file that may hold `llm-wiki` configuration, ordered
72
+ * from lowest to highest precedence so callers can merge left-to-right.
73
+ *
74
+ * The host's *native* directory is last (wins). The foreign directory is still
75
+ * read so a vault configured under pi keeps working after omp takes over the
76
+ * repository, and vice versa. Within a directory `config.yml` follows
77
+ * `settings.json`, matching omp's own project-settings precedence.
78
+ */
79
+ export function listProjectSettingsFiles(cwd: string, host: HostKind = detectHost()): string[] {
80
+ const foreign: HostKind = host === "omp" ? "pi" : "omp";
81
+ const files: string[] = [];
82
+ for (const kind of [foreign, host]) {
83
+ const dir = join(cwd, CONFIG_DIR[kind]);
84
+ for (const name of SETTINGS_FILES) files.push(join(dir, name));
85
+ }
86
+ return files;
87
+ }
88
+
89
+ /**
90
+ * User-level settings files, lowest → highest precedence.
91
+ *
92
+ * `getAgentDir()` already resolves per host (`~/.pi/agent` vs `~/.omp/agent`),
93
+ * so only the file names differ: omp migrates `settings.json` into `config.yml`
94
+ * on first start, and a migrated install has *only* the YAML file.
95
+ */
96
+ export function listGlobalSettingsFiles(): string[] {
97
+ let agentDir = "";
98
+ try {
99
+ agentDir = getAgentDir();
100
+ } catch {
101
+ return [];
102
+ }
103
+ if (!agentDir) return [];
104
+ return SETTINGS_FILES.map((name) => join(agentDir, name));
105
+ }
106
+
107
+ /**
108
+ * The project settings file this extension writes to.
109
+ *
110
+ * Always JSON (`settings.json`) — both hosts read it, and rewriting a user's
111
+ * hand-authored `config.yml` would destroy comments and formatting.
112
+ *
113
+ * Directory choice: an already-existing project config directory wins (so a
114
+ * repo that only has `.pi/` keeps a single settings file), otherwise the
115
+ * detected host's native directory is created.
116
+ */
117
+ export function resolveProjectSettingsPath(cwd: string, host: HostKind = detectHost()): string {
118
+ const native = join(cwd, CONFIG_DIR[host]);
119
+ if (existsSync(native)) return join(native, "settings.json");
120
+
121
+ const foreign = join(cwd, CONFIG_DIR[host === "omp" ? "pi" : "omp"]);
122
+ if (existsSync(foreign)) return join(foreign, "settings.json");
123
+
124
+ return join(native, "settings.json");
125
+ }
@@ -322,11 +322,20 @@ export function buildReminderText(): string {
322
322
  * `options.display` (issue #77) controls whether the reminder is shown to the
323
323
  * user (`true`, the default) or injected silently into model context only
324
324
  * (`false`). Pass a resolver so the live `notices` config is read at send time.
325
+ *
326
+ * `options.enabled` gates the reminder entirely — note that `display: false`
327
+ * still injects it into model context, so it is NOT a way to switch the
328
+ * reminder off. Callers pass a resolver that answers "does a wiki apply to the
329
+ * current working directory", evaluated per turn because the session can move.
325
330
  */
326
331
  export function registerObservationReminder(
327
332
  pi: ExtensionAPI,
328
333
  reminderState: ReminderState,
329
- options?: { turnsBetweenReminders?: number; display?: boolean | (() => boolean) },
334
+ options?: {
335
+ turnsBetweenReminders?: number;
336
+ display?: boolean | (() => boolean);
337
+ enabled?: () => boolean;
338
+ },
330
339
  ): void {
331
340
  const REMINDER_INTERVAL = options?.turnsBetweenReminders ?? 5;
332
341
  const resolveDisplay = (): boolean => {
@@ -355,6 +364,10 @@ export function registerObservationReminder(
355
364
  // errors cause multiple retries, each firing agent_end).
356
365
  if ("willRetry" in event && (event as { willRetry?: boolean }).willRetry) return;
357
366
 
367
+ // No wiki applies here: never nag, and never accumulate a pending reminder
368
+ // that would fire the moment the session moves into a wiki-bearing project.
369
+ if (options?.enabled && !options.enabled()) return;
370
+
358
371
  turnsSinceLastReminder++;
359
372
  if (turnsSinceLastReminder < REMINDER_INTERVAL) return;
360
373
  if (reminderState.observeDoneThisSession) return;
@@ -213,8 +213,20 @@ export class Runtime {
213
213
  */
214
214
  launchReported(ctx: LaunchCtx, label: string, work: () => Promise<string | null>): Promise<void> {
215
215
  return this.launchTask(ctx, label, async () => {
216
+ // Capture synchronously — after `await work()` the extension ctx may be
217
+ // a stale proxy (newSession/fork/switchSession/reload) and accessing
218
+ // ctx.hasUI or ctx.ui on it throws (see launchTask).
219
+ const hasUI = ctx.hasUI;
220
+ const ui = ctx.ui;
216
221
  const summary = await work();
217
- if (summary) this.report(summary);
222
+ if (summary) {
223
+ // Instant completion feedback: the nextTurn report below is queued for
224
+ // the next user prompt, so without a toast a background task looks
225
+ // stuck. Mirrors the failure notification in launchTask and the
226
+ // success toast already used by wiki_ingest.
227
+ if (hasUI && ui) ui.notify(summary.split("\n")[0].replace(/\*\*/g, ""), "info");
228
+ this.report(summary);
229
+ }
218
230
  });
219
231
  }
220
232