pi-vault-mind 0.16.28 → 0.16.30

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/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.30 — 2026-08-27
4
+
5
+ ### Added
6
+
7
+ - **First-class `pvm` executable.** The npm package now exposes `pvm run [vault-path]`; it uses the current directory when the vault path is omitted and supports both global installation and `npx pi-vault-mind run`.
8
+ - **Explicit launch modes.** `pvm run` stays in the current terminal by default, with `--same-terminal`, `--new-window`, and `--auto` overrides.
9
+
10
+ ### Fixed
11
+
12
+ - **Portable vault launcher.** `vault-pi.sh` now handles interactive terminals directly, normalizes relative vault paths before deriving runtime state, removes inline-session PID files on exit, and rejects unknown flags instead of treating them as vault paths.
13
+
14
+ ### Verification
15
+
16
+ - Root and Obsidian builds passed; full test suite: 910/910 passing.
17
+ - `npm pack` includes `scripts/pvm.mjs` as an executable, and `npm exec` resolved the packed tarball's `pvm` command successfully.
18
+
19
+ ## 0.16.29 — 2026-08-27
20
+
21
+ ### Fixed
22
+
23
+ - **Main-terminal identity isolation.** Ambient bundled specialist skills no longer make the interactive Pi session inherit the first matching specialist boundary. Identity injection now defaults to the unrestricted main agent and only activates from an explicit vault or environment selection.
24
+ - **Explicit persona controls.** Added `/vm identity status | set <role> | on | off`, `VAULT_MIND_AGENT_ROLE`, and `VAULT_MIND_IDENTITY_INJECTION`, with canonical persistence under `vaultMind.identities`.
25
+ - **Identity configuration migration and write safety.** Legacy top-level identity settings migrate without losing role allowlists, new scaffolds use the canonical path, and malformed JSON is reported without overwriting the existing vault configuration or leaking an unhandled HTTP rejection.
26
+
27
+ ### Verification
28
+
29
+ - Root and Obsidian builds passed; full test suite: 899/899 passing.
30
+ - Real-vault startup probe confirmed ambient Broadcaster resources keep the unrestricted main identity while an explicit Broadcaster override activates the boundary.
3
31
 
4
32
  ## 0.16.28 — 2026-08-09
5
33
 
package/README.md CHANGED
@@ -194,10 +194,25 @@ The Broadcaster agent can generate podcasts, study guides, and slide decks from
194
194
 
195
195
  ```bash
196
196
  pi install npm:pi-vault-mind # latest
197
- pi install npm:pi-vault-mind@0.7.0 # pinned
197
+ pi install npm:pi-vault-mind@0.16.30 # pinned release
198
198
  pi -e npm:pi-vault-mind # try without installing
199
199
  ```
200
200
 
201
+ `pi install` loads the extension but does not expose its npm executable on your
202
+ shell `PATH`. Install the first-class launcher once when you want the bare
203
+ `pvm` command:
204
+
205
+ ```bash
206
+ npm install --global pi-vault-mind
207
+ pvm run /path/to/your/vault
208
+ ```
209
+
210
+ Or run it without a global installation:
211
+
212
+ ```bash
213
+ npx --yes pi-vault-mind run /path/to/your/vault
214
+ ```
215
+
201
216
  Or from git:
202
217
 
203
218
  ```bash
@@ -269,6 +284,174 @@ Edit `<vault>/.vault-mind/vault-mind.config.json` to match your domain:
269
284
  }
270
285
  ```
271
286
 
287
+ ## Using Vault Mind agent identities
288
+
289
+ Vault Mind keeps the interactive Pi session on the unrestricted `main` persona
290
+ unless you explicitly select a specialist boundary. Bundled specialist skills
291
+ are available resources; merely loading them does not change the active
292
+ persona.
293
+
294
+ ### Start the vault-local main agent
295
+
296
+ From the vault directory, start Pi in the current terminal:
297
+
298
+ ```bash
299
+ pvm run
300
+ ```
301
+
302
+ You can launch a different vault from anywhere or use the package without a
303
+ global install:
304
+
305
+ ```bash
306
+ pvm run /path/to/your/vault
307
+ npx --yes pi-vault-mind run /path/to/your/vault
308
+ ```
309
+
310
+ `pvm run` defaults to the current terminal. This is the recommended Obsidian
311
+ Terminal profile command. Use a vault-path placeholder supplied by the plugin,
312
+ or provide the vault's absolute path. Override the launch mode when needed:
313
+
314
+ ```bash
315
+ pvm run --same-terminal /path/to/your/vault
316
+ pvm run --new-window /path/to/your/vault
317
+ pvm run --auto /path/to/your/vault
318
+ ```
319
+
320
+ `--new-window` opens a dedicated Terminal/iTerm window. `--auto` uses the
321
+ current terminal when attached to a TTY and falls back to a new window
322
+ otherwise. The packaged shell launcher remains available for direct use as
323
+ `scripts/vault-pi.sh`.
324
+
325
+ In every mode, the launcher runs Pi inside the target vault with its
326
+ configuration isolated under `<vault>/.vault-mind/.pi/agent/`. It also prevents
327
+ two launcher-managed Pi processes from running for the same vault and removes
328
+ the PID file when an inline session exits.
329
+
330
+ Check the active persona after startup:
331
+
332
+ ```text
333
+ /vm identity status
334
+ ```
335
+
336
+ A normal main-agent session reports:
337
+
338
+ ```text
339
+ Persona: main
340
+ Effective boundary: none (main agent)
341
+ ```
342
+
343
+ `Injection` may report `enabled` or `disabled`. An enabled injector still
344
+ applies no boundary while the persona is `main`.
345
+
346
+ ### Use specialist agents without changing the main session
347
+
348
+ Normally, leave the interactive session on `main` and dispatch specialist work
349
+ through the passive watcher:
350
+
351
+ ```text
352
+ @agent-Miner
353
+ Research and extract the durable facts from this note.
354
+ ```
355
+
356
+ The watcher runs that work as an isolated specialist job. It does not turn the
357
+ interactive terminal into the Miner persona. The same distinction applies to
358
+ `@agent-Broadcaster`, `@agent-Manager`, and `@agent-Heavy-Lifter`.
359
+
360
+ ### Explicitly change the interactive persona
361
+
362
+ Use an identity boundary only when the current interactive session itself
363
+ should follow a specialist capability contract:
364
+
365
+ ```text
366
+ /vm identity set miner
367
+ /vm identity set broadcaster
368
+ /vm identity set manager
369
+ /vm identity set heavy-lifter
370
+ ```
371
+
372
+ | Persona | Intended interactive boundary |
373
+ |---|---|
374
+ | `main` | Unrestricted primary agent with the full configured toolset |
375
+ | `manager` | Orchestrates collections, publishing, and subagents; does not run Bash |
376
+ | `miner` | Extracts and stores knowledge; does not run Bash or publish |
377
+ | `broadcaster` | Produces presentation artifacts; does not run Bash or write durable knowledge |
378
+ | `heavy-lifter` | Performs file and Bash work in isolation; does not write durable knowledge or publish |
379
+
380
+ `/vm identity set <role>` persists the selected persona, enables boundary
381
+ injection, and applies on the next agent turn.
382
+
383
+ Use the remaining controls to inspect or suspend that selection:
384
+
385
+ ```text
386
+ /vm identity status
387
+ /vm identity off
388
+ /vm identity on
389
+ ```
390
+
391
+ - `off` disables boundary injection without discarding the selected persona.
392
+ - `on` re-enables the configured persona boundary on the next agent turn.
393
+ - `status` shows the configured persona, injection switch, effective boundary,
394
+ and any environment overrides.
395
+
396
+ Restore the safe main-agent default with:
397
+
398
+ ```text
399
+ /vm identity set main
400
+ /vm identity off
401
+ /vm identity status
402
+ ```
403
+
404
+ The final status should show `Effective boundary: none (main agent)`.
405
+
406
+ ### Configuration and environment overrides
407
+
408
+ Identity controls are stored in the vault-local configuration:
409
+
410
+ ```json
411
+ {
412
+ "vaultMind": {
413
+ "identities": {
414
+ "activeRole": "main",
415
+ "injectBoundaries": false
416
+ }
417
+ }
418
+ }
419
+ ```
420
+
421
+ The canonical path is
422
+ `<vault>/.vault-mind/vault-mind.config.json` under
423
+ `vaultMind.identities`. Identity commands preserve any existing
424
+ `vaultMind.identities.roles` allowlists.
425
+
426
+ Launch automation and explicit specialist forks can override the saved values
427
+ for one process:
428
+
429
+ ```bash
430
+ VAULT_MIND_AGENT_ROLE=miner VAULT_MIND_IDENTITY_INJECTION=on pi
431
+ VAULT_MIND_IDENTITY_INJECTION=off pi
432
+ ```
433
+
434
+ `VAULT_MIND_AGENT_ROLE` takes precedence over `activeRole`.
435
+ `VAULT_MIND_IDENTITY_INJECTION=off` suppresses every boundary, while `on`
436
+ enables the explicitly selected role. Missing, `main`, `none`, and unknown
437
+ roles all resolve safely to the unrestricted main agent.
438
+
439
+ ### Troubleshoot an unexpected specialist boundary
440
+
441
+ 1. Run `/vm identity status` and check both the configured persona and any
442
+ reported environment overrides.
443
+ 2. If an environment override is present, exit Pi, unset
444
+ `VAULT_MIND_AGENT_ROLE` and `VAULT_MIND_IDENTITY_INJECTION`, then relaunch.
445
+ 3. Run `/vm identity set main`, followed by `/vm identity off`.
446
+ 4. Send the next agent turn and re-run `/vm identity status`; boundary changes
447
+ take effect on the next turn.
448
+ 5. Confirm Pi was started for the intended vault and uses
449
+ `<vault>/.vault-mind/.pi/agent/` as `PI_CODING_AGENT_DIR`.
450
+
451
+ Do not edit `SYSTEM.md` or an agent skill to change the interactive identity.
452
+ Those files provide operating instructions and resources; persona selection
453
+ belongs in `vaultMind.identities` or an explicit environment override.
454
+
272
455
  ## Example domains
273
456
 
274
457
  ### Research project
@@ -356,6 +539,7 @@ Edit `<vault>/.vault-mind/vault-mind.config.json` to match your domain:
356
539
  | `/vm injector create` | Interactive wizard to create a new injector |
357
540
  | `/vm context enable \| disable \| status` | Manage pi-context integration |
358
541
  | `/vm embedding status \| use \| model \| models \| pull` | Manage embedding provider |
542
+ | `/vm identity status \| set <role> \| on \| off` | Inspect or explicitly control the interactive agent persona boundary |
359
543
  | `/vm remote status \| config \| sync \| jobs \| migrate` | Manage remote embedding + vector sync |
360
544
  | `/vm watcher start \| stop \| status` | Manage the passive file watcher |
361
545
  | `/vm server status` | Show HTTP server status, port, and uptime |
@@ -8,6 +8,8 @@ import { revokeToken, rotateToken } from "./auth.js";
8
8
  import { handleDiscoverSchema } from "./discover-schema.js";
9
9
  import { writeEnvEntry } from "./embedding-secrets.js";
10
10
  import { queryGraph } from "./graph.js";
11
+ import { writeIdentitiesLayer } from "./identities-config.js";
12
+ import { IDENTITY_BOUNDARY_ROLES, resolveIdentityBoundaryRole } from "./identity-injector.js";
11
13
  import { runAskIntake } from "./intake.js";
12
14
  import { connect, searchFts, searchHybrid, searchHybridRanked } from "./lance.js";
13
15
  import { createModalClient, isModalConfigured, MODAL_TOKEN_ENV, modalTokenEnvPath, resolveModalToken, } from "./modal-config.js";
@@ -39,6 +41,9 @@ const VM_USAGE = [
39
41
  " /vm index [--all] [--reembed] [--remote] Index collections (alias: reindex)",
40
42
  " /vm discover-schema Infer schema from a JSONL file",
41
43
  " /vm injector create Create a new injector (wizard)",
44
+ " /vm identity status Show configured and effective persona",
45
+ " /vm identity set <role> Set main|manager|miner|broadcaster|heavy-lifter",
46
+ " /vm identity on|off Enable or disable persona boundary injection",
42
47
  " /vm context enable|disable Enable/disable pi-context integration",
43
48
  " /vm embedding status Show embedding config",
44
49
  " /vm remote status Show remote provider config + health + collections",
@@ -1616,6 +1621,74 @@ const handleToken = (args, ctx) => {
1616
1621
  ctx.ui.notify(`Unknown: ${sub}. Try: show, rotate, revoke`, "error");
1617
1622
  }
1618
1623
  };
1624
+ const handleIdentity = (args, ctx) => {
1625
+ const parts = args.trim().split(/\s+/g).filter(Boolean);
1626
+ const subcommand = parts[0]?.toLowerCase() || "status";
1627
+ const identities = loadConfig(ctx.cwd).vaultMind.identities;
1628
+ const configuredRole = identities?.activeRole?.trim().toLowerCase() || "main";
1629
+ const updateIdentity = (patch) => {
1630
+ try {
1631
+ writeIdentitiesLayer(ctx.cwd, patch);
1632
+ return true;
1633
+ }
1634
+ catch (error) {
1635
+ const message = error instanceof Error ? error.message : String(error);
1636
+ ctx.ui.notify(`Failed to update identity config: ${message}`, "error");
1637
+ return false;
1638
+ }
1639
+ };
1640
+ switch (subcommand) {
1641
+ case "status": {
1642
+ const effectiveRole = resolveIdentityBoundaryRole(ctx.cwd);
1643
+ const injectionEnabled = identities?.injectBoundaries !== false;
1644
+ const envRole = process.env.VAULT_MIND_AGENT_ROLE?.trim();
1645
+ const envInjection = process.env.VAULT_MIND_IDENTITY_INJECTION?.trim();
1646
+ ctx.ui.notify([
1647
+ "**Identity Boundary**",
1648
+ `Persona: ${configuredRole}`,
1649
+ `Injection: ${injectionEnabled ? "enabled" : "disabled"}`,
1650
+ `Effective boundary: ${effectiveRole ?? "none (main agent)"}`,
1651
+ ...(envRole ? [`Environment role override: ${envRole}`] : []),
1652
+ ...(envInjection ? [`Environment injection override: ${envInjection}`] : []),
1653
+ ].join("\n"), "info");
1654
+ return;
1655
+ }
1656
+ case "set": {
1657
+ const role = parts[1]?.toLowerCase();
1658
+ const validRole = role === "main" ||
1659
+ (role !== undefined &&
1660
+ IDENTITY_BOUNDARY_ROLES.includes(role));
1661
+ if (!role || !validRole) {
1662
+ ctx.ui.notify(`Unknown persona: ${role ?? "(missing)"}. Use main, ${IDENTITY_BOUNDARY_ROLES.join(", ")}.`, "error");
1663
+ return;
1664
+ }
1665
+ if (!updateIdentity({
1666
+ activeRole: role,
1667
+ injectBoundaries: true,
1668
+ })) {
1669
+ return;
1670
+ }
1671
+ ctx.ui.notify(`Persona set to ${role}. The boundary applies on the next agent turn.`, "info");
1672
+ return;
1673
+ }
1674
+ case "off":
1675
+ case "disable": {
1676
+ if (!updateIdentity({ injectBoundaries: false }))
1677
+ return;
1678
+ ctx.ui.notify("Identity injection disabled. The main agent remains unrestricted.", "info");
1679
+ return;
1680
+ }
1681
+ case "on":
1682
+ case "enable": {
1683
+ if (!updateIdentity({ injectBoundaries: true }))
1684
+ return;
1685
+ ctx.ui.notify(`Identity injection enabled for ${configuredRole}. The setting applies on the next agent turn.`, "info");
1686
+ return;
1687
+ }
1688
+ default:
1689
+ ctx.ui.notify("Usage: /vm identity status | set <role> | on | off", "error");
1690
+ }
1691
+ };
1619
1692
  // ── Main /vm command ───────────────────────────────────────────────────────
1620
1693
  export const registerCommands = (pi) => {
1621
1694
  pi.registerCommand("vm", {
@@ -1633,6 +1706,7 @@ export const registerCommands = (pi) => {
1633
1706
  "discover-schema",
1634
1707
  "doctor",
1635
1708
  "injector",
1709
+ "identity",
1636
1710
  "context",
1637
1711
  "embedding",
1638
1712
  "remote",
@@ -1662,6 +1736,11 @@ export const registerCommands = (pi) => {
1662
1736
  .filter((c) => c.startsWith(prefix))
1663
1737
  .map((c) => ({ label: c, value: c, description: `context ${c}` }));
1664
1738
  }
1739
+ if (subcommand === "identity") {
1740
+ return ["status", "set", "on", "off", "main", ...IDENTITY_BOUNDARY_ROLES]
1741
+ .filter((c) => c.startsWith(prefix))
1742
+ .map((c) => ({ label: c, value: c, description: `identity ${c}` }));
1743
+ }
1665
1744
  if (subcommand === "discover-schema") {
1666
1745
  return ["--name", "--sample"]
1667
1746
  .filter((c) => c.startsWith(prefix))
@@ -1736,6 +1815,8 @@ export const registerCommands = (pi) => {
1736
1815
  return handleCollection(rest, ctx, pi);
1737
1816
  case "injector":
1738
1817
  return handleInjector(rest, ctx);
1818
+ case "identity":
1819
+ return handleIdentity(rest, ctx);
1739
1820
  case "context":
1740
1821
  return handleContext(rest, ctx, pi);
1741
1822
  case "embedding":
@@ -26,11 +26,27 @@ function deepMerge(base, overlay) {
26
26
  */
27
27
  export function writeIdentitiesLayer(cwd, identities) {
28
28
  const configPath = getConfigPath(cwd);
29
- const existing = fs.existsSync(configPath)
30
- ? JSON.parse(fs.readFileSync(configPath, "utf-8"))
31
- : {};
29
+ let existing = {};
30
+ if (fs.existsSync(configPath)) {
31
+ const raw = fs.readFileSync(configPath, "utf-8");
32
+ try {
33
+ const parsed = JSON.parse(raw);
34
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
35
+ throw new TypeError("the root value must be a JSON object");
36
+ }
37
+ existing = parsed;
38
+ }
39
+ catch (error) {
40
+ throw new Error(`Invalid JSON in ${configPath}; the file was not changed.`, {
41
+ cause: error,
42
+ });
43
+ }
44
+ }
32
45
  const vaultMind = existing.vaultMind || {};
33
- vaultMind.identities = deepMerge(vaultMind.identities || {}, identities);
46
+ const legacyIdentities = existing.identities || {};
47
+ const canonicalIdentities = vaultMind.identities || {};
48
+ vaultMind.identities = deepMerge(deepMerge(legacyIdentities, canonicalIdentities), identities);
49
+ delete existing.identities;
34
50
  existing.vaultMind = vaultMind;
35
51
  const dir = path.dirname(configPath);
36
52
  if (!fs.existsSync(dir))
@@ -6,10 +6,21 @@
6
6
  * from pi-guideline-loader (kylebrodeur/pi-guideline-loader, MIT).
7
7
  */
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+ export declare const IDENTITY_BOUNDARY_ROLES: readonly ["heavy-lifter", "miner", "broadcaster", "manager"];
10
+ export type IdentityBoundaryRole = (typeof IDENTITY_BOUNDARY_ROLES)[number];
11
+ /**
12
+ * Resolve an explicitly selected specialist persona.
13
+ *
14
+ * Specialist skills are ambient resources in every pi session, so their
15
+ * presence must never select a persona. The environment can override the
16
+ * vault-local config for launcher/fork use. Unset, "main", "none", and invalid
17
+ * roles all keep the unrestricted interactive main agent.
18
+ */
19
+ export declare function resolveIdentityBoundaryRole(cwd: string, env?: NodeJS.ProcessEnv): IdentityBoundaryRole | undefined;
9
20
  /**
10
21
  * Register the identity injector on a pi ExtensionAPI.
11
22
  *
12
- * On every before_agent_start event, detects the current role and injects
13
- * the corresponding boundary prompt into the system prompt.
23
+ * On every before_agent_start event, injects a boundary only when a specialist
24
+ * persona was selected explicitly through environment or vault config.
14
25
  */
15
26
  export declare function registerIdentityInjector(pi: ExtensionAPI): void;
@@ -5,7 +5,9 @@
5
5
  * per role stating capability boundaries as a contract. Pattern adapted
6
6
  * from pi-guideline-loader (kylebrodeur/pi-guideline-loader, MIT).
7
7
  */
8
+ import { loadConfig } from "./utils.js";
8
9
  // ── Per-role boundary prompts ────────────────────────────────────────────────
10
+ export const IDENTITY_BOUNDARY_ROLES = ["heavy-lifter", "miner", "broadcaster", "manager"];
9
11
  const BOUNDARY_PROMPTS = {
10
12
  "heavy-lifter": [
11
13
  "--- IDENTITY BOUNDARY: Heavy-Lifter ---",
@@ -50,46 +52,58 @@ const BOUNDARY_PROMPTS = {
50
52
  "------------------------------------------",
51
53
  ].join("\n"),
52
54
  };
53
- // ── Role detection ────────────────────────────────────────────────────────────
55
+ // ── Role resolution ──────────────────────────────────────────────────────────
56
+ const DISABLED_SWITCH_VALUES = {
57
+ "0": true,
58
+ false: true,
59
+ no: true,
60
+ off: true,
61
+ disabled: true,
62
+ };
63
+ const ENABLED_SWITCH_VALUES = {
64
+ "1": true,
65
+ true: true,
66
+ yes: true,
67
+ on: true,
68
+ enabled: true,
69
+ };
54
70
  /**
55
- * Detect the current agent's role from a before_agent_start event.
56
- *
57
- * PRIMARY signal — event.systemPromptOptions.skills:
58
- * pi loads `--agent vault-mind-{role}` as a named Skill (Skill.name == "vault-mind-{role}").
59
- * Exact name match is structural and immune to prose false-positives.
60
- *
61
- * FALLBACK signal — system prompt XML:
62
- * Only when no matching skill is present. Matches only the machine-generated
63
- * `<name>vault-mind-{role}</name>` element emitted by formatSkillsForPrompt()
64
- * inside <available_skills> — not any free-text prose mention of the role name.
71
+ * Resolve an explicitly selected specialist persona.
65
72
  *
66
- * Returns undefined for any non-vault-mind session; no injection occurs.
73
+ * Specialist skills are ambient resources in every pi session, so their
74
+ * presence must never select a persona. The environment can override the
75
+ * vault-local config for launcher/fork use. Unset, "main", "none", and invalid
76
+ * roles all keep the unrestricted interactive main agent.
67
77
  */
68
- function detectRole(event) {
69
- // PRIMARY: exact skill name match — most reliable, no prose false-positive risk.
70
- const skills = event.systemPromptOptions.skills;
71
- if (skills) {
72
- for (const skill of skills) {
73
- const m = skill.name.match(/^vault-mind-(heavy-lifter|miner|broadcaster|manager)$/i);
74
- if (m)
75
- return m[1].toLowerCase();
76
- }
78
+ export function resolveIdentityBoundaryRole(cwd, env = process.env) {
79
+ const identities = loadConfig(cwd).vaultMind.identities;
80
+ const injectionOverride = env.VAULT_MIND_IDENTITY_INJECTION?.trim().toLowerCase();
81
+ if (injectionOverride && DISABLED_SWITCH_VALUES[injectionOverride])
82
+ return;
83
+ if ((!injectionOverride || !ENABLED_SWITCH_VALUES[injectionOverride]) &&
84
+ identities?.injectBoundaries === false) {
85
+ return;
77
86
  }
78
- // FALLBACK: only the assembled system prompt is available — match the XML
79
- // <name> token produced by formatSkillsForPrompt(), not free prose.
80
- const m = event.systemPrompt.match(/<name>vault-mind-(heavy-lifter|miner|broadcaster|manager)<\/name>/i);
81
- return m?.[1]?.toLowerCase();
87
+ const rawRole = Object.hasOwn(env, "VAULT_MIND_AGENT_ROLE")
88
+ ? env.VAULT_MIND_AGENT_ROLE
89
+ : identities?.activeRole;
90
+ const role = rawRole?.trim().toLowerCase();
91
+ if (!role || role === "main" || role === "none")
92
+ return;
93
+ if (!Object.hasOwn(BOUNDARY_PROMPTS, role))
94
+ return;
95
+ return role;
82
96
  }
83
97
  // ── Injector ──────────────────────────────────────────────────────────────────
84
98
  /**
85
99
  * Register the identity injector on a pi ExtensionAPI.
86
100
  *
87
- * On every before_agent_start event, detects the current role and injects
88
- * the corresponding boundary prompt into the system prompt.
101
+ * On every before_agent_start event, injects a boundary only when a specialist
102
+ * persona was selected explicitly through environment or vault config.
89
103
  */
90
104
  export function registerIdentityInjector(pi) {
91
105
  pi.on("before_agent_start", async (event) => {
92
- const role = detectRole(event);
106
+ const role = resolveIdentityBoundaryRole(event.systemPromptOptions.cwd);
93
107
  if (!role)
94
108
  return;
95
109
  const boundaryPrompt = BOUNDARY_PROMPTS[role];
@@ -135,37 +135,37 @@ export const scaffoldVaultConfig = (vaultPath, collectionOverride) => {
135
135
  version: 2,
136
136
  collections: defaultCollections,
137
137
  injectors: defaultInjectors,
138
- identities: {
139
- roles: {
140
- main: {
141
- allowedTools: [
142
- "read",
143
- "write",
144
- "edit",
145
- "bash",
146
- "grep",
147
- "find",
148
- "ls",
149
- "vm_search",
150
- "vm_query",
151
- "vm_append",
152
- "vm_stats",
153
- "vm_status",
154
- "vm_describe",
155
- "vm_configure",
156
- "vault_read",
157
- "vault_search",
158
- "vault_list",
159
- "vault_tags",
160
- "vm_backlinks",
161
- "vm_related",
162
- "vm_broken_links",
163
- "vm_codegraph_impact",
164
- ],
138
+ vaultMind: {
139
+ identities: {
140
+ roles: {
141
+ main: {
142
+ allowedTools: [
143
+ "read",
144
+ "write",
145
+ "edit",
146
+ "bash",
147
+ "grep",
148
+ "find",
149
+ "ls",
150
+ "vm_search",
151
+ "vm_query",
152
+ "vm_append",
153
+ "vm_stats",
154
+ "vm_status",
155
+ "vm_describe",
156
+ "vm_configure",
157
+ "vault_read",
158
+ "vault_search",
159
+ "vault_list",
160
+ "vault_tags",
161
+ "vm_backlinks",
162
+ "vm_related",
163
+ "vm_broken_links",
164
+ "vm_codegraph_impact",
165
+ ],
166
+ },
165
167
  },
166
168
  },
167
- },
168
- vaultMind: {
169
169
  dataDir: ".vault-mind/.lancedb",
170
170
  ftsEnabled: true,
171
171
  graph: { enabled: true, canvasSync: false },
@@ -832,19 +832,28 @@ export function startServer(pi, serverState, watcherState, readinessCallback) {
832
832
  if (parts.length === 5) {
833
833
  const role = parts[3];
834
834
  withAuth(req, res, async () => {
835
- const body = (await readJsonBody(req));
836
- if (!body.allowedTools || !Array.isArray(body.allowedTools)) {
837
- res.writeHead(400, { "Content-Type": "application/json" });
838
- res.end(JSON.stringify({ error: "Missing allowedTools array in request body" }));
839
- return;
835
+ try {
836
+ const body = (await readJsonBody(req));
837
+ if (!body.allowedTools || !Array.isArray(body.allowedTools)) {
838
+ res.writeHead(400, { "Content-Type": "application/json" });
839
+ res.end(JSON.stringify({ error: "Missing allowedTools array in request body" }));
840
+ return;
841
+ }
842
+ // writeIdentitiesLayer deep-merges; pass a partial role config
843
+ writeIdentitiesLayer(process.cwd(), {
844
+ roles: { [role]: { allowedTools: body.allowedTools } },
845
+ });
846
+ refreshAgentIdentities([role]);
847
+ res.writeHead(200, { "Content-Type": "application/json" });
848
+ res.end(JSON.stringify({ ok: true, role, allowedTools: body.allowedTools }));
849
+ }
850
+ catch (error) {
851
+ const message = error instanceof Error ? error.message : String(error);
852
+ res.writeHead(500, { "Content-Type": "application/json" });
853
+ res.end(JSON.stringify({
854
+ error: `Failed to update identity config: ${message}`,
855
+ }));
840
856
  }
841
- // writeIdentitiesLayer deep-merges; pass a partial role config
842
- writeIdentitiesLayer(process.cwd(), {
843
- roles: { [role]: { allowedTools: body.allowedTools } },
844
- });
845
- refreshAgentIdentities([role]);
846
- res.writeHead(200, { "Content-Type": "application/json" });
847
- res.end(JSON.stringify({ ok: true, role, allowedTools: body.allowedTools }));
848
857
  }, { requireWrite: true });
849
858
  }
850
859
  else {
@@ -203,6 +203,10 @@ export interface VaultMindConfig {
203
203
  defaultProfile?: IdentityConfig;
204
204
  /** Per-role overrides. */
205
205
  roles?: Record<string, IdentityConfig>;
206
+ /** Master switch for system-prompt identity-boundary injection. */
207
+ injectBoundaries?: boolean;
208
+ /** Explicit persona to inject; unset, "main", or "none" keeps the main agent. */
209
+ activeRole?: string;
206
210
  };
207
211
  /**
208
212
  * Vault folder layout overrides. Each path is vault-relative and joined to
@@ -235,6 +239,10 @@ export interface IdentitiesConfig {
235
239
  defaultProfile?: IdentityConfig;
236
240
  /** Per-role overrides. */
237
241
  roles?: Record<string, IdentityConfig>;
242
+ /** Master switch for system-prompt identity-boundary injection. */
243
+ injectBoundaries?: boolean;
244
+ /** Explicit persona to inject; unset, "main", or "none" keeps the main agent. */
245
+ activeRole?: string;
238
246
  }
239
247
  export interface PiContextDef {
240
248
  enabled?: boolean;