pi-vault-mind 0.16.34 → 0.16.36

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 (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +32 -11
  3. package/dist/src/agent-docs.js +55 -0
  4. package/dist/src/agent-queue.js +3 -2
  5. package/dist/src/agents/BroadcasterAgent.js +3 -2
  6. package/dist/src/agents/HeavyLifterAgent.js +3 -2
  7. package/dist/src/agents/ManagerAgent.js +3 -2
  8. package/dist/src/agents/MinerAgent.js +3 -2
  9. package/dist/src/commands.js +101 -5
  10. package/dist/src/dispatch-writeback.js +20 -0
  11. package/dist/src/docling.js +43 -0
  12. package/dist/src/engine.js +2 -0
  13. package/dist/src/personalize.js +8 -2
  14. package/dist/src/tool-catalog.js +1 -0
  15. package/dist/src/tools.js +57 -27
  16. package/dist/src/vm-handlers.js +10 -20
  17. package/dist/src/vm-script.js +201 -0
  18. package/dist/src/watcher.js +164 -62
  19. package/dist/test/agent-docs.test.js +28 -0
  20. package/dist/test/agent-queue.test.js +9 -8
  21. package/dist/test/agents/Agents.test.js +6 -5
  22. package/dist/test/check-deps-script.test.js +8 -1
  23. package/dist/test/commands.test.js +43 -0
  24. package/dist/test/index.test.js +24 -0
  25. package/dist/test/personalize.test.js +63 -0
  26. package/dist/test/pvm-setup.test.js +19 -0
  27. package/dist/test/tool-catalog.test.js +10 -4
  28. package/dist/test/vm-script-tool.test.js +21 -0
  29. package/package.json +3 -2
  30. package/scripts/check-deps.sh +5 -5
  31. package/scripts/pvm-setup.mjs +39 -8
  32. package/skills/vault-mind/SKILL.md +29 -3
  33. package/skills/vault-mind-miner/SKILL.md +5 -5
  34. package/skills/vault-mind-setup/references/obsidian-vault-structure.md +1 -1
  35. package/skills/vault-mind-setup/references/pi-extension-wiring.md +12 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.36 — 2026-08-30
4
+
5
+ ### Changed
6
+
7
+ - **Node 24 runtime floor.** The supported Node.js floor is now `24.19.0+`, matching the current local/runtime toolchain and simplifying support expectations.
8
+ - **GitHub workflows disabled.** The repository no longer runs the generic `ci.yml` or `release-obsidian.yml` automation. Verification and publishing are currently a local operator workflow; the former workflow files are preserved under `.github/workflows-disabled/` for future reuse.
9
+
10
+ ## 0.16.35 — 2026-08-30
11
+
12
+ ### Added
13
+
14
+ - **Watcher request-block preservation.** The vault watcher no longer destructively replaces `@agent` markers with subagent output. The original request block is preserved in the source note; only the running-status line is updated with a brief completion summary. Requests are reviewable and the vault note remains a faithful record of what was asked.
15
+ - **Reserved `@agent:main` identity.** The `@agent:main` marker is recognized by the watcher but never auto-dispatched to a background subagent. It is reserved for the interactive main session. Use `/vm main sweep [file-or-folder]` to collect pending `@agent:main` markers and address them in the current session.
16
+ - **Stock `system.md` and `AGENTS.md` baseline provisioning.** `pvm setup` provisions vault-local `system.md` (agent system prompt) and vault-root `AGENTS.md` (agent conventions) from curated stock baselines when those files do not already exist, giving every fresh vault a working agent identity without manual authoring. Baselines are demarcated with `<!-- vm:baseline:start/end -->` tags so `/vm personalize` can update only the personalization section without touching the baseline.
17
+ - **`vm_script` tool and run-sdk scaffold.** A sandboxed JavaScript/TypeScript execution tool (`vm_script`) is scaffolded using the `run` QuickJS package. Scripts execute with no ambient filesystem, network, or timer access beyond declared host functions (`vm.*`, `vault.read`, `log`). Enabled by default for manager and main agents. Follow-up: HITL interruption path, `vault.write`, and `pvm run <script>` CLI subcommand.
18
+
19
+ ### Changed
20
+
21
+ - **docling replaces any2md.** The Miner's document-conversion path (`vm_ingest`) now uses `docling` (via `uvx` or a globally installed `pip install docling`) for PDF, DOCX, HTML, and URL normalization. The `any2md` npm package is no longer a dependency or runtime requirement.
22
+
23
+
3
24
  ## 0.16.34 — 2026-08-29
4
25
 
5
26
  ### Added
package/README.md CHANGED
@@ -12,7 +12,7 @@ Passive Obsidian vault extension for the [pi](https://github.com/mariozechner/pi
12
12
 
13
13
  ## Features
14
14
 
15
- - **Passive File Watcher** — drop `@agent-Miner`, `@agent-Broadcaster`, etc. in any Obsidian note. Save the file. The watcher detects the marker, groups by role, and dispatches isolated subagent forks (`vault-mind-{role}` agents). No chat pollution, no manual triggers.
15
+ - **Passive File Watcher** — drop `@agent:<role>` (or `@agent-<role>`) markers in any Obsidian note. Save. The watcher detects the marker, groups by role, and dispatches isolated subagent forks. The original request block is preserved in the note; only the running-status line is updated with the result summary. `@agent:main` is reserved for the current interactive session and is never auto-dispatched — sweep it manually with `/vm main sweep`. No chat pollution, no manual triggers.
16
16
  - **Multi-Agent Architecture** — five specialist agents under the `vault-mind-{role}` skill naming: **Manager** (interactive orchestrator), **Miner** (research + entity extraction), **Broadcaster** (NotebookLM artifacts), **Heavy-Lifter** (external delegation), **Watcher** (passive file observer).
17
17
  - **LanceDB Vector + FTS + Graph** — hybrid semantic + keyword search with automatic entity extraction and BFS graph traversal. All local, no external binaries.
18
18
  - **JSONL Source-of-Truth + LanceDB Index** — every fact lives in a durable, human-readable, version-control-friendly `collections/*.jsonl` file. The LanceDB index is derived and rebuildable via `/vm reindex --all --reembed`.
@@ -171,7 +171,7 @@ The Broadcaster agent can generate podcasts, study guides, and slide decks from
171
171
 
172
172
  ### Prerequisites
173
173
 
174
- - **Node.js** 22.19.0+ (matches `engines` in `package.json`)
174
+ - **Node.js** 24.19.0+ (matches `engines` in `package.json`)
175
175
  - **Embedding Provider** — choose one:
176
176
  - `@xenova/transformers` — built-in, no external deps (uses all-MiniLM-L6-v2, offline-capable)
177
177
  - `ollama` — requires Ollama running locally with `embeddinggemma` (higher quality)
@@ -200,8 +200,10 @@ context, model-routing, task, and subagent extensions inside
200
200
  `<vault>/.vault-mind/.pi/agent/`; installs the curated `obsidian-cli`,
201
201
  `obsidian-markdown`, `obsidian-bases`, and `json-canvas` skills; and
202
202
  merge-safely scaffolds Vault Mind, model-router, and main-agent configuration.
203
- Existing user configuration, collections, model choices, and `system.md` are
204
- preserved.
203
+ On first run it also creates stock `.pi/agent/system.md` and vault-root
204
+ `AGENTS.md` baseline regions. All of these are preserved on repeat runs —
205
+ existing user configuration, collections, model choices, and personalizations
206
+ are never overwritten.
205
207
 
206
208
  Refresh only extensions, tools, and skills without touching vault
207
209
  configuration:
@@ -360,16 +362,33 @@ applies no boundary while the persona is `main`.
360
362
  ### Use specialist agents without changing the main session
361
363
 
362
364
  Normally, leave the interactive session on `main` and dispatch specialist work
363
- through the passive watcher:
365
+ through the passive watcher. Both marker forms are equivalent:
364
366
 
365
367
  ```text
366
- @agent-Miner
368
+ @agent:miner
367
369
  Research and extract the durable facts from this note.
368
370
  ```
369
371
 
370
- The watcher runs that work as an isolated specialist job. It does not turn the
371
- interactive terminal into the Miner persona. The same distinction applies to
372
- `@agent-Broadcaster`, `@agent-Manager`, and `@agent-Heavy-Lifter`.
372
+ or
373
+
374
+ ```text
375
+ @agent-miner
376
+ Research and extract the durable facts from this note.
377
+ ```
378
+
379
+ The watcher runs that work as an isolated specialist job, preserving the
380
+ request block in the source note and replacing only the running-status line
381
+ with a result summary. It does not turn the interactive terminal into the
382
+ Miner persona. The same applies to `@agent:broadcaster`, `@agent:manager`,
383
+ and `@agent:heavy-lifter`.
384
+
385
+ `@agent:main` is reserved for the current interactive session and is never
386
+ auto-dispatched. To sweep reserved markers into the active session:
387
+
388
+ ```text
389
+ /vm main sweep
390
+ /vm main sweep path/to/note.md
391
+ ```
373
392
 
374
393
  ### Explicitly change the interactive persona
375
394
 
@@ -535,7 +554,8 @@ belongs in `vaultMind.identities` or an explicit environment override.
535
554
  | `vm_describe` | Introspect schema, count, and sample entries |
536
555
  | `vm_stats` | Dashboard: counts, sizes, LanceDB status |
537
556
  | `vm_export` | Export to JSON, CSV, or Markdown |
538
- | `vm_promote` | Promote entries between collections via pending queue |
557
+ | `vm_ingest` | Convert URLs and documents to markdown via docling |
558
+ | `vm_script` | Execute sandboxed JS/TS with vm.*, vault.read, log.* |
539
559
 
540
560
  ## Commands
541
561
 
@@ -555,7 +575,8 @@ belongs in `vaultMind.identities` or an explicit environment override.
555
575
  | `/vm embedding status \| use \| model \| models \| pull` | Manage embedding provider |
556
576
  | `/vm identity status \| set <role> \| on \| off` | Inspect or explicitly control the interactive agent persona boundary |
557
577
  | `/vm remote status \| config \| sync \| jobs \| migrate` | Manage remote embedding + vector sync |
558
- | `/vm watcher start \| stop \| status` | Manage the passive file watcher |
578
+ | `/vm watcher start \| stop \| status` | Manage the passive file watcher |
579
+ | `/vm main sweep [file]` | Sweep `@agent:main` markers into the current interactive session |
559
580
  | `/vm server status` | Show HTTP server status, port, and uptime |
560
581
 
561
582
  ## Documentation
@@ -0,0 +1,55 @@
1
+ import * as path from "node:path";
2
+ export const BASELINE_START = "<!-- vm:baseline:start -->";
3
+ export const BASELINE_END = "<!-- vm:baseline:end -->";
4
+ export const PERSONALIZATION_START = "<!-- vm:personalization:start -->";
5
+ export const PERSONALIZATION_END = "<!-- vm:personalization:end -->";
6
+ export const buildDefaultSystemMd = (vaultPath) => {
7
+ const vaultName = path.basename(vaultPath);
8
+ return `# Pi main agent: ${vaultName}
9
+
10
+ ${BASELINE_START}
11
+ You are the unrestricted main interactive agent for this vault. Bundled specialist skills are available resources; their presence does not change your active identity. Adopt a specialist identity only when explicitly launched as that subagent.
12
+
13
+ ## Operating baseline
14
+ - Read \`00-system/SYSTEM.md\` first when it exists. If it does not exist, fall back to a vault-root \`SYSTEM.md\`. Then read the vault-root \`AGENTS.md\`.
15
+ - Prefer the vault's own operating manual and schemas over generic assumptions.
16
+ - Use the \`vm_*\` tools, collections, and queue as the durable system of record for agent work.
17
+ - Prefer \`uv\` for Python tools and CLIs. Use \`pnpm\`/\`npm\` for JavaScript and TypeScript packages such as \`run\`.
18
+ - Use \`docling\` for document conversion in ingest flows.
19
+ - Use \`vm_script\` for reusable sandboxed JS/TS workflows when the task fits the approved host-function surface.
20
+ - Never auto-publish, never fabricate facts, and keep human approval at external side effects.
21
+ - The watcher recognizes \`@agent:<role>\` and \`@agent-<role>\` anywhere in watchable files. \`@agent:main\` is reserved for the main interactive flow and is not auto-dispatched.
22
+ ${BASELINE_END}
23
+
24
+ ${PERSONALIZATION_START}
25
+ Add vault-specific priorities, domain language, and working preferences here. /vm personalize may update only this section.
26
+ ${PERSONALIZATION_END}
27
+ `;
28
+ };
29
+ export const buildDefaultAgentsMd = () => `# AGENTS: pi-vault-mind operating baseline
30
+
31
+ ${BASELINE_START}
32
+ ## Agent roles
33
+ - \`main\` — the interactive agent you work with directly.
34
+ - \`manager\` — synthesis, orchestration, and roadmap work.
35
+ - \`miner\` — extraction, ingestion, and structured knowledge capture.
36
+ - \`broadcaster\` — human-facing artifact generation.
37
+ - \`heavy-lifter\` — large-context implementation and deeper delegated work.
38
+ - \`personalization\` — resolves annotated feedback on generated system/agent docs.
39
+
40
+ ## Watcher + inline requests
41
+ - Use \`@agent:<role>\` or \`@agent-<role>\` in watchable files to request agent help.
42
+ - \`@agent:main\` is recognized but never auto-dispatched; it is for main-agent or human-led follow-up.
43
+ - Auto-dispatched watcher jobs preserve the request block in the source note and replace only the running-status line with the result summary.
44
+ - Watcher dispatches should be logged to collections so they can be queried later.
45
+
46
+ ## Tooling baseline
47
+ - Prefer \`uv\` for Python tools.
48
+ - Prefer reusable \`vm_script\` workflows over ad-hoc one-off scripts when the workflow should be repeatable.
49
+ - Keep reusable content, test, and update workflows in skills/tools rather than scattered shell snippets.
50
+ ${BASELINE_END}
51
+
52
+ ${PERSONALIZATION_START}
53
+ Add vault-specific agent roster details, domain-specific prompts, and delegation rules here. /vm personalize may update only this section.
54
+ ${PERSONALIZATION_END}
55
+ `;
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
+ import { dispatchStatusLine } from "./dispatch-writeback.js";
15
16
  import { loadConfig, resolveVaultFile } from "./utils.js";
16
17
  const queueSubscribers = new Set();
17
18
  /** Subscribe to queue events. Returns an unsubscribe function. */
@@ -138,11 +139,11 @@ export const cancelJob = (vaultPath, id) => {
138
139
  return undefined;
139
140
  return updateJob(vaultPath, id, "cancelled");
140
141
  };
141
- /** True if the file still contains the dispatch marker for this job. */
142
+ /** True if the file still contains the pending dispatch status placeholder. */
142
143
  export const markerStillPresent = (filePath, dispatchId) => {
143
144
  try {
144
145
  const content = fs.readFileSync(filePath, "utf-8");
145
- return content.includes(`<!-- pi-dispatch:${dispatchId} -->`);
146
+ return content.includes(dispatchStatusLine(dispatchId));
146
147
  }
147
148
  catch {
148
149
  return false;
@@ -6,6 +6,7 @@
6
6
  * Writes output only under Agent/Presentations/.
7
7
  */
8
8
  import { Agent } from "../agent-bus/Agent.js";
9
+ import { buildDispatchResultLine, dispatchStatusLine } from "../dispatch-writeback.js";
9
10
  export class BroadcasterAgent extends Agent {
10
11
  taskQueue = null;
11
12
  constructor(config, bus, llm) {
@@ -58,8 +59,8 @@ export class BroadcasterAgent extends Agent {
58
59
  path: job.filePath,
59
60
  edits: [
60
61
  {
61
- oldText: `<!-- pi-dispatch:${job.dispatchId} -->`,
62
- newText: `> Broadcaster completed. See dispatch ${job.dispatchId}.`,
62
+ oldText: dispatchStatusLine(job.dispatchId),
63
+ newText: buildDispatchResultLine(job.dispatchId, `Broadcaster completed. See dispatch ${job.dispatchId}.`),
63
64
  },
64
65
  ],
65
66
  });
@@ -6,6 +6,7 @@
6
6
  * Runs bash in isolated worktree.
7
7
  */
8
8
  import { Agent } from "../agent-bus/Agent.js";
9
+ import { buildDispatchResultLine, dispatchStatusLine } from "../dispatch-writeback.js";
9
10
  export class HeavyLifterAgent extends Agent {
10
11
  taskQueue = null;
11
12
  constructor(config, bus, llm) {
@@ -61,8 +62,8 @@ export class HeavyLifterAgent extends Agent {
61
62
  path: job.filePath,
62
63
  edits: [
63
64
  {
64
- oldText: `<!-- pi-dispatch:${job.dispatchId} -->`,
65
- newText: `> Heavy-Lifter completed. See dispatch ${job.dispatchId}.`,
65
+ oldText: dispatchStatusLine(job.dispatchId),
66
+ newText: buildDispatchResultLine(job.dispatchId, `Heavy-Lifter completed. See dispatch ${job.dispatchId}.`),
66
67
  },
67
68
  ],
68
69
  });
@@ -7,6 +7,7 @@
7
7
  * Only the Manager can dispatch subagents and publish to the human-facing vault.
8
8
  */
9
9
  import { Agent } from "../agent-bus/Agent.js";
10
+ import { buildDispatchResultLine, dispatchStatusLine } from "../dispatch-writeback.js";
10
11
  export class ManagerAgent extends Agent {
11
12
  taskQueue = null;
12
13
  constructor(config, bus, llm) {
@@ -59,8 +60,8 @@ export class ManagerAgent extends Agent {
59
60
  path: job.filePath,
60
61
  edits: [
61
62
  {
62
- oldText: `<!-- pi-dispatch:${job.dispatchId} -->`,
63
- newText: `> Manager completed. See dispatch ${job.dispatchId}.`,
63
+ oldText: dispatchStatusLine(job.dispatchId),
64
+ newText: buildDispatchResultLine(job.dispatchId, `Manager completed. See dispatch ${job.dispatchId}.`),
64
65
  },
65
66
  ],
66
67
  });
@@ -6,6 +6,7 @@
6
6
  * Cannot publish (vm_sync disabled).
7
7
  */
8
8
  import { Agent } from "../agent-bus/Agent.js";
9
+ import { buildDispatchResultLine, dispatchStatusLine } from "../dispatch-writeback.js";
9
10
  export class MinerAgent extends Agent {
10
11
  taskQueue = null;
11
12
  constructor(config, bus, llm) {
@@ -58,8 +59,8 @@ export class MinerAgent extends Agent {
58
59
  path: job.filePath,
59
60
  edits: [
60
61
  {
61
- oldText: `<!-- pi-dispatch:${job.dispatchId} -->`,
62
- newText: `> Miner extracted findings. See dispatch ${job.dispatchId}.`,
62
+ oldText: dispatchStatusLine(job.dispatchId),
63
+ newText: buildDispatchResultLine(job.dispatchId, `Miner extracted findings. See dispatch ${job.dispatchId}.`),
63
64
  },
64
65
  ],
65
66
  });
@@ -19,11 +19,11 @@ import { scaffoldVaultConfig } from "./scaffold.js";
19
19
  import { createServerState, startServer } from "./server.js";
20
20
  import { createCollectionWizard, createInjectorWizard, openSettingsDashboard, setupWizard, } from "./settings-ui.js";
21
21
  import { getActiveCollection, setActiveCollection } from "./state.js";
22
+ import { createWatcherState, getWatcherStatus, isWatchableFile, scanFile, startWatcher, stopWatcher } from "./watcher.js";
22
23
  import { reindexRemote, syncAll, syncCollection } from "./sync.js";
23
24
  import { tombstoneEntryById } from "./tombstone.js";
24
25
  import { DEFAULT_CONFIG } from "./types.js";
25
26
  import { collectionNames, ensureDir, expandHome, findConfig, getConfigPath, getPiContextConfig, hasPiContextTools, isVaultMindReady, loadConfig, resolveVaultMindPaths, } from "./utils.js";
26
- import { createWatcherState, getWatcherStatus, startWatcher, stopWatcher } from "./watcher.js";
27
27
  import { updateActiveCollectionWidget } from "./widget.js";
28
28
  // ── Shared helpers ───────────────────────────────────────────────────────────
29
29
  const VM_USAGE = [
@@ -51,15 +51,15 @@ const VM_USAGE = [
51
51
  " /vm remote auto Discover models from the configured Remote URL",
52
52
  " /vm remote sync Pull server vectors into local LanceDB [--full]",
53
53
  " /vm remote jobs <id> Poll a remote bulk embed job",
54
- " /vm watcher start Start passive file watcher",
54
+ " /vm watcher start [roles] Start passive file watcher (optional role allowlist)",
55
55
  " /vm watcher stop Stop passive file watcher",
56
56
  " /vm watcher status Show watcher status",
57
57
  " /vm watcher queue [status] Show agent dispatch queue (pending|running|done|failed|cancelled|all)",
58
58
  " /vm watcher retry <id> Retry a failed or cancelled agent job",
59
59
  " /vm watcher cancel <id> Cancel a pending agent job",
60
+ " /vm main sweep [file] Sweep @agent:main markers into the current session",
60
61
  " /vm server status Show HTTP server status + port",
61
62
  " /vm server start Start the in-process HTTP server",
62
- " /vm personalize Analyze vault and suggest personality tuning for system.md + AGENTS.md",
63
63
  " /vm setup Onboarding flow: config + personalize (auto-uses cwd when it's a vault)",
64
64
  " /vm setup --vault <path> CLI: set vault path",
65
65
  " /vm setup --remoteUrl <url> CLI: set remote embedding URL",
@@ -1297,6 +1297,90 @@ const handleServer = (args, ctx, pi) => {
1297
1297
  }
1298
1298
  ctx.ui.notify(lines.join("\n"), "info");
1299
1299
  };
1300
+ const MAIN_SWEEP_IGNORE_DIRS = {
1301
+ ".git": true,
1302
+ ".obsidian": true,
1303
+ node_modules: true,
1304
+ ".trash": true,
1305
+ __pycache__: true,
1306
+ ".DS_Store": true,
1307
+ ".pi": true,
1308
+ ".lancedb": true,
1309
+ };
1310
+ const collectSweepFiles = (targetPath) => {
1311
+ const stat = fs.statSync(targetPath);
1312
+ if (stat.isFile())
1313
+ return isWatchableFile(targetPath) ? [targetPath] : [];
1314
+ if (!stat.isDirectory())
1315
+ return [];
1316
+ const files = [];
1317
+ for (const entry of fs.readdirSync(targetPath, { withFileTypes: true })) {
1318
+ if (MAIN_SWEEP_IGNORE_DIRS[entry.name])
1319
+ continue;
1320
+ const child = path.join(targetPath, entry.name);
1321
+ if (entry.isDirectory()) {
1322
+ files.push(...collectSweepFiles(child));
1323
+ }
1324
+ else if (entry.isFile() && isWatchableFile(child)) {
1325
+ files.push(child);
1326
+ }
1327
+ }
1328
+ return files;
1329
+ };
1330
+ const handleMain = async (args, ctx, pi) => {
1331
+ const parts = args.trim().split(/\s+/g).filter(Boolean);
1332
+ const subcommand = parts[0]?.toLowerCase() || "sweep";
1333
+ if (subcommand !== "sweep") {
1334
+ ctx.ui.notify("Usage: /vm main sweep [file-or-folder]", "error");
1335
+ return;
1336
+ }
1337
+ const configuredVault = loadConfig(ctx.cwd).vaultMind.vaults?.default?.path;
1338
+ const vaultRoot = configuredVault ? expandHome(configuredVault) : ctx.cwd;
1339
+ const targetArg = parts.slice(1).join(" ").trim();
1340
+ const targetPath = targetArg
1341
+ ? path.isAbsolute(targetArg)
1342
+ ? targetArg
1343
+ : path.join(ctx.cwd, targetArg)
1344
+ : vaultRoot;
1345
+ if (!fs.existsSync(targetPath)) {
1346
+ ctx.ui.notify(`Main sweep target not found: ${targetPath}`, "error");
1347
+ return;
1348
+ }
1349
+ const items = collectSweepFiles(targetPath)
1350
+ .flatMap((filePath) => scanFile(filePath, vaultRoot).flatMap((group) => group.role !== "main"
1351
+ ? []
1352
+ : group.markers.map((marker) => ({
1353
+ filePath,
1354
+ lineNumber: marker.lineNumber,
1355
+ instruction: marker.instruction || marker.rawLine.trim(),
1356
+ context: marker.context,
1357
+ }))));
1358
+ if (items.length === 0) {
1359
+ ctx.ui.notify("No unresolved @agent:main markers found.", "info");
1360
+ return;
1361
+ }
1362
+ const prompt = [
1363
+ "# Main Agent Sweep",
1364
+ "",
1365
+ "Review the following inline @agent:main requests. These are reserved for the current interactive main session and are not auto-dispatched.",
1366
+ "Address the requests directly in this session. Keep the source notes intact unless an edit is explicitly needed.",
1367
+ "",
1368
+ ...items.flatMap((item, index) => [
1369
+ `## Item ${index + 1}`,
1370
+ `- File: ${item.filePath}`,
1371
+ `- Line: ${item.lineNumber}`,
1372
+ `- Request: ${item.instruction}`,
1373
+ "",
1374
+ "Context:",
1375
+ "```",
1376
+ item.context,
1377
+ "```",
1378
+ "",
1379
+ ]),
1380
+ ].join("\n");
1381
+ pi.sendMessage({ customType: "vm-main-sweep", content: prompt, display: false }, { triggerTurn: true });
1382
+ ctx.ui.notify(`Sweeping ${items.length} @agent:main item(s) from ${new Set(items.map((item) => item.filePath)).size} file(s).`, "info");
1383
+ };
1300
1384
  const handleWatcher = async (args, ctx, pi) => {
1301
1385
  const parts = args.trim().split(/\s+/g);
1302
1386
  const subcommand = parts[0]?.toLowerCase() || "status";
@@ -1316,7 +1400,12 @@ const handleWatcher = async (args, ctx, pi) => {
1316
1400
  ctx.ui.notify("Watcher is already running.", "info");
1317
1401
  return;
1318
1402
  }
1319
- startWatcher(pi, cfg.vaultMind.vaults, watcherState);
1403
+ const requestedRoles = parts
1404
+ .slice(1)
1405
+ .flatMap((part) => part.split(","))
1406
+ .map((role) => role.trim().toLowerCase())
1407
+ .filter(Boolean);
1408
+ startWatcher(pi, cfg.vaultMind.vaults, watcherState, requestedRoles.length > 0 ? requestedRoles : undefined);
1320
1409
  ctx.ui.notify(getWatcherStatus(watcherState), "info");
1321
1410
  return;
1322
1411
  }
@@ -1711,8 +1800,8 @@ export const registerCommands = (pi) => {
1711
1800
  "embedding",
1712
1801
  "remote",
1713
1802
  "watcher",
1803
+ "main",
1714
1804
  "personalize",
1715
- "setup",
1716
1805
  "token",
1717
1806
  "index",
1718
1807
  "reindex",
@@ -1741,6 +1830,11 @@ export const registerCommands = (pi) => {
1741
1830
  .filter((c) => c.startsWith(prefix))
1742
1831
  .map((c) => ({ label: c, value: c, description: `identity ${c}` }));
1743
1832
  }
1833
+ if (subcommand === "main") {
1834
+ return ["sweep"]
1835
+ .filter((c) => c.startsWith(prefix))
1836
+ .map((c) => ({ label: c, value: c, description: `main ${c}` }));
1837
+ }
1744
1838
  if (subcommand === "discover-schema") {
1745
1839
  return ["--name", "--sample"]
1746
1840
  .filter((c) => c.startsWith(prefix))
@@ -1825,6 +1919,8 @@ export const registerCommands = (pi) => {
1825
1919
  return handleServer(rest, ctx, pi);
1826
1920
  case "watcher":
1827
1921
  return handleWatcher(rest, ctx, pi);
1922
+ case "main":
1923
+ return handleMain(rest, ctx, pi);
1828
1924
  case "remote":
1829
1925
  return handleModal(rest, ctx, pi);
1830
1926
  case "personalize":
@@ -0,0 +1,20 @@
1
+ export const dispatchMarker = (dispatchId) => `<!-- pi-dispatch:${dispatchId} -->`;
2
+ export const dispatchStatusLine = (dispatchId) => `> Status [${dispatchId}]: running`;
3
+ const summarizeInstruction = (instruction) => {
4
+ const trimmed = instruction.trim();
5
+ return trimmed.length > 0 ? trimmed : "(no instruction provided)";
6
+ };
7
+ export const buildPrimaryDispatchBlock = (dispatchId, role, instructions, markerCount) => {
8
+ const lines = [dispatchMarker(dispatchId), `> Agent: ${role}`];
9
+ if (markerCount > 1) {
10
+ lines.push(`> Grouped markers: ${markerCount}`);
11
+ }
12
+ lines.push("> Requests:");
13
+ for (const instruction of instructions) {
14
+ lines.push(`> - ${summarizeInstruction(instruction)}`);
15
+ }
16
+ lines.push(dispatchStatusLine(dispatchId));
17
+ return lines.join("\n");
18
+ };
19
+ export const buildJoinedDispatchNote = (dispatchId, role, instruction, lineNumber) => `> Joined dispatch ${dispatchId} (${role}, line ${lineNumber}): ${summarizeInstruction(instruction)}`;
20
+ export const buildDispatchResultLine = (dispatchId, result) => `> Result [${dispatchId}]: ${result}`;
@@ -0,0 +1,43 @@
1
+ import { execFile } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { promisify } from "node:util";
6
+ const execFileAsync = promisify(execFile);
7
+ export const DOCLING_DOC_EXTS = {
8
+ ".pdf": true,
9
+ ".docx": true,
10
+ ".pptx": true,
11
+ ".xlsx": true,
12
+ ".epub": true,
13
+ ".html": true,
14
+ ".htm": true,
15
+ ".xml": true,
16
+ };
17
+ const doclingErrorMessage = (error) => error instanceof Error ? error.message : String(error);
18
+ export const sourceNeedsDocling = (source, sourceType) => {
19
+ if (sourceType === "url")
20
+ return true;
21
+ if (sourceType !== "file")
22
+ return false;
23
+ return DOCLING_DOC_EXTS[path.extname(source).toLowerCase()] === true;
24
+ };
25
+ export const convertWithDocling = async (source, cwd) => {
26
+ const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "pvm-docling-"));
27
+ try {
28
+ await execFileAsync("docling", ["convert", source, "--to", "md", "--output", outputDir, "--quiet"], { cwd, timeout: 120000 });
29
+ const markdownFile = fs
30
+ .readdirSync(outputDir)
31
+ .find((file) => path.extname(file).toLowerCase() === ".md");
32
+ if (!markdownFile) {
33
+ throw new Error(`docling did not produce a markdown file for ${source}`);
34
+ }
35
+ return fs.readFileSync(path.join(outputDir, markdownFile), "utf-8");
36
+ }
37
+ catch (error) {
38
+ throw new Error(`docling failed: ${doclingErrorMessage(error)}`);
39
+ }
40
+ finally {
41
+ fs.rmSync(outputDir, { recursive: true, force: true });
42
+ }
43
+ };
@@ -217,6 +217,7 @@ export function buildIdentityFromRole(role, id) {
217
217
  "vm_codegraph_impact",
218
218
  "vault_write",
219
219
  "vault_append",
220
+ "vm_script",
220
221
  ],
221
222
  writeCollections: ["main", "research", "presentations"],
222
223
  canPublish: true,
@@ -248,6 +249,7 @@ export function buildIdentityFromRole(role, id) {
248
249
  "vm_related",
249
250
  "vm_broken_links",
250
251
  "vm_codegraph_impact",
252
+ "vm_script",
251
253
  ],
252
254
  readCollections: ["main"],
253
255
  writeCollections: [],
@@ -4,6 +4,7 @@ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
4
4
  import { notifyAcmReminder } from "./context-capture.js";
5
5
  import { getStatus } from "./lance.js";
6
6
  import { scaffoldVaultConfig } from "./scaffold.js";
7
+ import { BASELINE_END, BASELINE_START, PERSONALIZATION_END, PERSONALIZATION_START, buildDefaultAgentsMd, buildDefaultSystemMd, } from "./agent-docs.js";
7
8
  import { expandHome, getPersonalizedMarkerPath, hasPiContextTools, isPersonalized, loadConfig, resolveAgentDir, } from "./utils.js";
8
9
  class PersonalizationCancelledError extends Error {
9
10
  constructor() {
@@ -197,6 +198,8 @@ const buildPrompt = (profile, currentConfig) => {
197
198
  },
198
199
  ],
199
200
  };
201
+ const seededSystemMd = currentConfig.systemMd ?? buildDefaultSystemMd(profile.vaultPath);
202
+ const seededAgentsMd = currentConfig.agentsMd ?? buildDefaultAgentsMd();
200
203
  const lines = [
201
204
  "# Vault Personality Tuning",
202
205
  "",
@@ -222,12 +225,12 @@ const buildPrompt = (profile, currentConfig) => {
222
225
  "",
223
226
  "### .pi/agent/system.md",
224
227
  "```markdown",
225
- currentConfig.systemMd ?? "(file does not exist)",
228
+ seededSystemMd,
226
229
  "```",
227
230
  "",
228
231
  "### AGENTS.md",
229
232
  "```markdown",
230
- currentConfig.agentsMd ?? "(file does not exist)",
233
+ seededAgentsMd,
231
234
  "```",
232
235
  "",
233
236
  "## Instructions",
@@ -242,6 +245,9 @@ const buildPrompt = (profile, currentConfig) => {
242
245
  "- If a file does not currently exist, use action: create.",
243
246
  "- If a file exists but needs changes, use action: update and provide the full new content.",
244
247
  "- If a file is fine as-is, use action: skip and omit content.",
248
+ "- Keep all content between the baseline markers unchanged.",
249
+ `- The baseline markers are: ${BASELINE_START}, ${BASELINE_END}, ${PERSONALIZATION_START}, ${PERSONALIZATION_END}.`,
250
+ "- Put vault-specific tuning only inside the personalization section.",
245
251
  "- Tailor the voice, priorities, and conventions to the vault's domains and tags.",
246
252
  "- Keep files concise and actionable.",
247
253
  ];
@@ -46,6 +46,7 @@ const EXTENSION_TOOLS = [
46
46
  description: "Promote a pending entry to the main collection",
47
47
  },
48
48
  { name: "vm_ingest", kind: "vm_data", description: "Ingest content into a vault collection" },
49
+ { name: "vm_script", kind: "vm_data", description: "Execute sandboxed JS/TS against curated host functions" },
49
50
  { name: "vm_export", kind: "vm_data", description: "Export collection data" },
50
51
  {
51
52
  name: "vm_stats",