pi-vault-mind 0.16.28 → 0.16.29

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.29 — 2026-08-27
4
+
5
+ ### Fixed
6
+
7
+ - **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.
8
+ - **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`.
9
+ - **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.
10
+
11
+ ### Verification
12
+
13
+ - Root and Obsidian builds passed; full test suite: 899/899 passing.
14
+ - Real-vault startup probe confirmed ambient Broadcaster resources keep the unrestricted main identity while an explicit Broadcaster override activates the boundary.
3
15
 
4
16
  ## 0.16.28 — 2026-08-09
5
17
 
package/README.md CHANGED
@@ -356,6 +356,7 @@ Edit `<vault>/.vault-mind/vault-mind.config.json` to match your domain:
356
356
  | `/vm injector create` | Interactive wizard to create a new injector |
357
357
  | `/vm context enable \| disable \| status` | Manage pi-context integration |
358
358
  | `/vm embedding status \| use \| model \| models \| pull` | Manage embedding provider |
359
+ | `/vm identity status \| set <role> \| on \| off` | Inspect or explicitly control the interactive agent persona boundary |
359
360
  | `/vm remote status \| config \| sync \| jobs \| migrate` | Manage remote embedding + vector sync |
360
361
  | `/vm watcher start \| stop \| status` | Manage the passive file watcher |
361
362
  | `/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;
package/dist/src/utils.js CHANGED
@@ -89,6 +89,20 @@ const mergeInjectors = (base, layer) => {
89
89
  };
90
90
  const mergeConfigLayer = (base, layer, cwd) => {
91
91
  const rawLayer = layer;
92
+ const legacyIdentities = rawLayer.identities;
93
+ const canonicalIdentities = layer.vaultMind?.identities;
94
+ const mergedIdentities = base.vaultMind.identities || legacyIdentities || canonicalIdentities
95
+ ? {
96
+ ...base.vaultMind.identities,
97
+ ...legacyIdentities,
98
+ ...canonicalIdentities,
99
+ roles: {
100
+ ...base.vaultMind.identities?.roles,
101
+ ...legacyIdentities?.roles,
102
+ ...canonicalIdentities?.roles,
103
+ },
104
+ }
105
+ : undefined;
92
106
  const merged = {
93
107
  version: layer.version ?? base.version,
94
108
  collections: { ...base.collections },
@@ -99,6 +113,7 @@ const mergeConfigLayer = (base, layer, cwd) => {
99
113
  embedding: { ...base.vaultMind.embedding, ...(layer.vaultMind?.embedding || {}) },
100
114
  graph: { ...base.vaultMind.graph, ...(layer.vaultMind?.graph || {}) },
101
115
  vaults: { ...base.vaultMind.vaults, ...(layer.vaultMind?.vaults || {}) },
116
+ identities: mergedIdentities,
102
117
  },
103
118
  extensionCompatibility: {
104
119
  ...base.extensionCompatibility,
@@ -157,6 +157,13 @@ describe("CLI command handlers", () => {
157
157
  process.chdir(origCwd);
158
158
  fs.rmSync(dir, { recursive: true, force: true });
159
159
  });
160
+ describe("scaffoldVaultConfig", () => {
161
+ it("writes identities only at the canonical vaultMind path", () => {
162
+ const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
163
+ assert.equal(raw.identities, undefined);
164
+ assert.ok(raw.vaultMind.identities?.roles?.main);
165
+ });
166
+ });
160
167
  describe("handleSearch", () => {
161
168
  it("returns results in hybrid mode (default)", async () => {
162
169
  const vm = getVmHandler();
@@ -288,6 +295,77 @@ describe("CLI command handlers", () => {
288
295
  assert.match(last.message, /Unknown: bogus/);
289
296
  });
290
297
  });
298
+ describe("handleIdentity", () => {
299
+ it("reports main as the safe default persona", async () => {
300
+ const vm = getVmHandler();
301
+ await vm("identity status", makeCtx(dir, notifies));
302
+ const last = notifies.at(-1);
303
+ assert.ok(last, "notify was called");
304
+ assert.equal(last.level, "info");
305
+ assert.match(last.message, /Persona: main/);
306
+ assert.match(last.message, /Injection: enabled/);
307
+ });
308
+ it("sets an explicit persona and enables injection", async () => {
309
+ const vm = getVmHandler();
310
+ await vm("identity set broadcaster", makeCtx(dir, notifies));
311
+ const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
312
+ assert.equal(raw.vaultMind.identities.activeRole, "broadcaster");
313
+ assert.equal(raw.vaultMind.identities.injectBoundaries, true);
314
+ assert.match(notifies.at(-1)?.message ?? "", /Persona set to broadcaster/);
315
+ });
316
+ it("migrates legacy top-level identities when setting a persona", async () => {
317
+ const configPath = getConfigPath(dir);
318
+ const legacy = JSON.parse(fs.readFileSync(configPath, "utf-8"));
319
+ legacy.identities = legacy.vaultMind.identities;
320
+ delete legacy.vaultMind.identities;
321
+ fs.writeFileSync(configPath, `${JSON.stringify(legacy, null, 2)}\n`, "utf-8");
322
+ const vm = getVmHandler();
323
+ await vm("identity set miner", makeCtx(dir, notifies));
324
+ const migrated = JSON.parse(fs.readFileSync(configPath, "utf-8"));
325
+ assert.equal(migrated.identities, undefined);
326
+ assert.ok(migrated.vaultMind.identities.roles.main);
327
+ assert.equal(migrated.vaultMind.identities.activeRole, "miner");
328
+ });
329
+ it("turns injection off without discarding the selected persona", async () => {
330
+ const vm = getVmHandler();
331
+ await vm("identity set miner", makeCtx(dir, notifies));
332
+ await vm("identity off", makeCtx(dir, notifies));
333
+ const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
334
+ assert.equal(raw.vaultMind.identities.activeRole, "miner");
335
+ assert.equal(raw.vaultMind.identities.injectBoundaries, false);
336
+ assert.match(notifies.at(-1)?.message ?? "", /Identity injection disabled/);
337
+ });
338
+ it("turns injection back on for the configured persona", async () => {
339
+ const vm = getVmHandler();
340
+ await vm("identity set manager", makeCtx(dir, notifies));
341
+ await vm("identity off", makeCtx(dir, notifies));
342
+ await vm("identity on", makeCtx(dir, notifies));
343
+ const raw = JSON.parse(fs.readFileSync(getConfigPath(dir), "utf-8"));
344
+ assert.equal(raw.vaultMind.identities.activeRole, "manager");
345
+ assert.equal(raw.vaultMind.identities.injectBoundaries, true);
346
+ assert.match(notifies.at(-1)?.message ?? "", /Identity injection enabled/);
347
+ });
348
+ it("rejects an unknown persona without changing identity config", async () => {
349
+ const configPath = getConfigPath(dir);
350
+ const before = JSON.parse(fs.readFileSync(configPath, "utf-8")).vaultMind.identities;
351
+ const vm = getVmHandler();
352
+ await vm("identity set reviewer", makeCtx(dir, notifies));
353
+ const after = JSON.parse(fs.readFileSync(configPath, "utf-8")).vaultMind.identities;
354
+ assert.deepEqual(after, before);
355
+ assert.equal(notifies.at(-1)?.level, "error");
356
+ assert.match(notifies.at(-1)?.message ?? "", /Unknown persona/);
357
+ });
358
+ it("reports malformed config without overwriting it", async () => {
359
+ const configPath = getConfigPath(dir);
360
+ const malformed = "{ invalid json";
361
+ fs.writeFileSync(configPath, malformed, "utf-8");
362
+ const vm = getVmHandler();
363
+ await assert.doesNotReject(() => vm("identity set miner", makeCtx(dir, notifies)));
364
+ assert.equal(fs.readFileSync(configPath, "utf-8"), malformed);
365
+ assert.equal(notifies.at(-1)?.level, "error");
366
+ assert.match(notifies.at(-1)?.message ?? "", /Failed to update identity config/);
367
+ });
368
+ });
291
369
  describe("handleWatcher", () => {
292
370
  it("status shows watcher stopped by default", async () => {
293
371
  const vm = getVmHandler();
@@ -1,14 +1,33 @@
1
1
  import { strict as assert } from "node:assert";
2
- import { describe, it } from "node:test";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { afterEach, describe, it } from "node:test";
3
6
  import { registerIdentityInjector } from "../src/identity-injector.js";
4
- /** Create a mock pi that captures the before_agent_start handler. */
7
+ const ENV_KEYS = ["VAULT_MIND_AGENT_ROLE", "VAULT_MIND_IDENTITY_INJECTION"];
8
+ const originalEnv = {
9
+ VAULT_MIND_AGENT_ROLE: process.env.VAULT_MIND_AGENT_ROLE,
10
+ VAULT_MIND_IDENTITY_INJECTION: process.env.VAULT_MIND_IDENTITY_INJECTION,
11
+ };
12
+ const temporaryDirectories = [];
13
+ afterEach(() => {
14
+ for (const key of ENV_KEYS) {
15
+ const value = originalEnv[key];
16
+ if (value === undefined)
17
+ delete process.env[key];
18
+ else
19
+ process.env[key] = value;
20
+ }
21
+ while (temporaryDirectories.length > 0) {
22
+ fs.rmSync(temporaryDirectories.pop(), { recursive: true, force: true });
23
+ }
24
+ });
5
25
  function mockPi() {
6
26
  const captured = { current: null };
7
27
  const pi = {
8
28
  on(event, handler) {
9
- if (event === "before_agent_start") {
29
+ if (event === "before_agent_start")
10
30
  captured.current = handler;
11
- }
12
31
  },
13
32
  };
14
33
  return {
@@ -18,19 +37,27 @@ function mockPi() {
18
37
  },
19
38
  };
20
39
  }
21
- /** Build a minimal BeforeAgentStartEvent with optional skills. */
40
+ function temporaryVault(identities) {
41
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "pvm-identity-"));
42
+ temporaryDirectories.push(cwd);
43
+ if (identities) {
44
+ const configDirectory = path.join(cwd, ".vault-mind");
45
+ fs.mkdirSync(configDirectory, { recursive: true });
46
+ fs.writeFileSync(path.join(configDirectory, "vault-mind.config.json"), `${JSON.stringify({ version: 2, vaultMind: { identities } }, null, 2)}\n`, "utf-8");
47
+ }
48
+ return cwd;
49
+ }
22
50
  function makeEvent(opts) {
23
51
  return {
24
52
  type: "before_agent_start",
25
53
  prompt: "test prompt",
26
54
  systemPrompt: opts.systemPrompt ?? "base system prompt",
27
55
  systemPromptOptions: {
28
- cwd: "/test",
56
+ cwd: opts.cwd,
29
57
  skills: opts.skills,
30
58
  },
31
59
  };
32
60
  }
33
- /** Build a Skill with a given name. */
34
61
  function skill(name) {
35
62
  return {
36
63
  name,
@@ -41,157 +68,68 @@ function skill(name) {
41
68
  disableModelInvocation: false,
42
69
  };
43
70
  }
44
- // ── Tests ─────────────────────────────────────────────────────────────────────
71
+ async function invoke(event) {
72
+ const mock = mockPi();
73
+ registerIdentityInjector(mock.pi);
74
+ assert.ok(mock.handler, "before_agent_start handler should be registered");
75
+ return mock.handler(event);
76
+ }
45
77
  describe("registerIdentityInjector", () => {
46
- it("registers a before_agent_start hook on the pi instance", () => {
47
- const mock = mockPi();
48
- registerIdentityInjector(mock.pi);
49
- assert.ok(mock.handler, "handler should be registered");
50
- });
51
- it("the hook returns undefined for non-vault-mind sessions (no matching skill)", async () => {
52
- const mock = mockPi();
53
- registerIdentityInjector(mock.pi);
54
- assert.ok(mock.handler);
55
- const event = makeEvent({
56
- skills: [skill("some-other-skill"), skill("another-skill")],
57
- });
58
- const result = await mock.handler(event);
78
+ it("does not infer a persona from ambient specialist skills", async () => {
79
+ const cwd = temporaryVault();
80
+ const result = await invoke(makeEvent({
81
+ cwd,
82
+ skills: [
83
+ skill("vault-mind-broadcaster"),
84
+ skill("vault-mind-manager"),
85
+ skill("vault-mind-miner"),
86
+ ],
87
+ systemPrompt: "base prompt\n<available_skills><name>vault-mind-broadcaster</name></available_skills>",
88
+ }));
59
89
  assert.strictEqual(result, undefined);
60
90
  });
61
- it("the hook returns undefined when skills array is empty", async () => {
62
- const mock = mockPi();
63
- registerIdentityInjector(mock.pi);
64
- assert.ok(mock.handler);
65
- const event = makeEvent({ skills: [] });
66
- const result = await mock.handler(event);
67
- assert.strictEqual(result, undefined);
91
+ it("injects the persona selected in vault config", async () => {
92
+ const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
93
+ const result = await invoke(makeEvent({ cwd, systemPrompt: "base prompt" }));
94
+ assert.ok(result?.systemPrompt?.includes("base prompt"));
95
+ assert.ok(result?.systemPrompt?.includes("IDENTITY BOUNDARY: Broadcaster"));
68
96
  });
69
- it("the hook returns undefined when skills is undefined (no skills at all)", async () => {
70
- const mock = mockPi();
71
- registerIdentityInjector(mock.pi);
72
- assert.ok(mock.handler);
73
- const event = makeEvent({});
74
- const result = await mock.handler(event);
75
- assert.strictEqual(result, undefined);
97
+ it("treats main and none as the full-tool main persona", async () => {
98
+ for (const activeRole of ["main", "none"]) {
99
+ const cwd = temporaryVault({ injectBoundaries: true, activeRole });
100
+ const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
101
+ assert.strictEqual(result, undefined, activeRole);
102
+ }
76
103
  });
77
- it("the hook injects boundary prompt for heavy-lifter role", async () => {
78
- const mock = mockPi();
79
- registerIdentityInjector(mock.pi);
80
- assert.ok(mock.handler);
81
- const event = makeEvent({
82
- skills: [skill("vault-mind-heavy-lifter")],
83
- systemPrompt: "base prompt",
84
- });
85
- const result = await mock.handler(event);
86
- assert.ok(result);
87
- assert.ok("systemPrompt" in result);
88
- assert.ok(result.systemPrompt.includes("base prompt"));
89
- assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Heavy-Lifter"));
90
- assert.ok(result.systemPrompt.includes("durable knowledge store"));
91
- });
92
- it("the hook injects boundary prompt for miner role", async () => {
93
- const mock = mockPi();
94
- registerIdentityInjector(mock.pi);
95
- assert.ok(mock.handler);
96
- const event = makeEvent({
97
- skills: [skill("vault-mind-miner")],
98
- systemPrompt: "base prompt",
99
- });
100
- const result = await mock.handler(event);
101
- assert.ok(result);
102
- assert.ok("systemPrompt" in result);
103
- assert.ok(result.systemPrompt.includes("base prompt"));
104
- assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Miner"));
105
- assert.ok(result.systemPrompt.includes("'main' and 'research'"));
106
- });
107
- it("the hook injects boundary prompt for broadcaster role", async () => {
108
- const mock = mockPi();
109
- registerIdentityInjector(mock.pi);
110
- assert.ok(mock.handler);
111
- const event = makeEvent({
112
- skills: [skill("vault-mind-broadcaster")],
113
- systemPrompt: "base prompt",
114
- });
115
- const result = await mock.handler(event);
116
- assert.ok(result);
117
- assert.ok("systemPrompt" in result);
118
- assert.ok(result.systemPrompt.includes("base prompt"));
119
- assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Broadcaster"));
120
- assert.ok(result.systemPrompt.includes("Agent/Presentations"));
121
- });
122
- it("the hook injects boundary prompt for manager role", async () => {
123
- const mock = mockPi();
124
- registerIdentityInjector(mock.pi);
125
- assert.ok(mock.handler);
126
- const event = makeEvent({
127
- skills: [skill("vault-mind-manager")],
128
- systemPrompt: "base prompt",
129
- });
130
- const result = await mock.handler(event);
131
- assert.ok(result);
132
- assert.ok("systemPrompt" in result);
133
- assert.ok(result.systemPrompt.includes("base prompt"));
134
- assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Manager"));
135
- assert.ok(result.systemPrompt.includes("publish to the human-facing vault"));
136
- });
137
- // ── Fallback: XML-based role detection ────────────────────────────────────
138
- it("detects role via XML fallback when no skills match", async () => {
139
- const mock = mockPi();
140
- registerIdentityInjector(mock.pi);
141
- assert.ok(mock.handler);
142
- const event = makeEvent({
143
- skills: [skill("unrelated-skill")],
144
- systemPrompt: "base prompt\n<available_skills>\n<name>vault-mind-miner</name>\n</available_skills>",
145
- });
146
- const result = await mock.handler(event);
147
- assert.ok(result);
148
- assert.ok("systemPrompt" in result);
149
- assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Miner"));
104
+ it("config hard-off suppresses a configured specialist persona", async () => {
105
+ const cwd = temporaryVault({ injectBoundaries: false, activeRole: "broadcaster" });
106
+ const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
107
+ assert.strictEqual(result, undefined);
150
108
  });
151
- it("detects role via XML fallback when skills is undefined", async () => {
152
- const mock = mockPi();
153
- registerIdentityInjector(mock.pi);
154
- assert.ok(mock.handler);
155
- const event = makeEvent({
156
- systemPrompt: "base prompt\n<available_skills>\n<name>vault-mind-broadcaster</name>\n</available_skills>",
157
- });
158
- const result = await mock.handler(event);
159
- assert.ok(result);
160
- assert.ok("systemPrompt" in result);
161
- assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Broadcaster"));
109
+ it("environment role overrides the configured persona", async () => {
110
+ process.env.VAULT_MIND_AGENT_ROLE = "miner";
111
+ const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
112
+ const result = await invoke(makeEvent({ cwd }));
113
+ assert.ok(result?.systemPrompt?.includes("IDENTITY BOUNDARY: Miner"));
114
+ assert.ok(!result?.systemPrompt?.includes("IDENTITY BOUNDARY: Broadcaster"));
162
115
  });
163
- it("returns undefined when XML fallback has no vault-mind role", async () => {
164
- const mock = mockPi();
165
- registerIdentityInjector(mock.pi);
166
- assert.ok(mock.handler);
167
- const event = makeEvent({
168
- systemPrompt: "base prompt\n<available_skills>\n<name>some-other-skill</name>\n</available_skills>",
169
- });
170
- const result = await mock.handler(event);
116
+ it("environment hard-off overrides both environment role and config", async () => {
117
+ process.env.VAULT_MIND_AGENT_ROLE = "miner";
118
+ process.env.VAULT_MIND_IDENTITY_INJECTION = "off";
119
+ const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
120
+ const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
171
121
  assert.strictEqual(result, undefined);
172
122
  });
173
- // ── Case insensitivity ────────────────────────────────────────────────────
174
- it("matches role case-insensitively (HEAVY-LIFTER)", async () => {
175
- const mock = mockPi();
176
- registerIdentityInjector(mock.pi);
177
- assert.ok(mock.handler);
178
- const event = makeEvent({
179
- skills: [skill("vault-mind-HEAVY-LIFTER")],
180
- });
181
- const result = await mock.handler(event);
182
- assert.ok(result);
183
- assert.ok("systemPrompt" in result);
184
- assert.ok(result.systemPrompt.includes("IDENTITY BOUNDARY: Heavy-Lifter"));
123
+ it("normalizes case and whitespace for explicit roles", async () => {
124
+ process.env.VAULT_MIND_AGENT_ROLE = " HEAVY-LIFTER ";
125
+ const cwd = temporaryVault();
126
+ const result = await invoke(makeEvent({ cwd }));
127
+ assert.ok(result?.systemPrompt?.includes("IDENTITY BOUNDARY: Heavy-Lifter"));
185
128
  });
186
- // ── Unknown role ──────────────────────────────────────────────────────────
187
- it("returns undefined for an unknown vault-mind role", async () => {
188
- const mock = mockPi();
189
- registerIdentityInjector(mock.pi);
190
- assert.ok(mock.handler);
191
- const event = makeEvent({
192
- skills: [skill("vault-mind-unknown-role")],
193
- });
194
- const result = await mock.handler(event);
129
+ it("ignores an unknown explicit persona rather than injecting the wrong one", async () => {
130
+ process.env.VAULT_MIND_AGENT_ROLE = "unknown-role";
131
+ const cwd = temporaryVault({ injectBoundaries: true, activeRole: "broadcaster" });
132
+ const result = await invoke(makeEvent({ cwd, skills: [skill("vault-mind-broadcaster")] }));
195
133
  assert.strictEqual(result, undefined);
196
134
  });
197
135
  });
@@ -461,4 +461,28 @@ describe("REST vm routes", () => {
461
461
  assert.ok(Array.isArray(returned), "GET must return a config with an injectors array");
462
462
  assert.ok(!returned.some((ij) => ij.name === "draft-context"), "GET must not resurrect the default injector after an explicit empty patch");
463
463
  });
464
+ it("returns JSON 500 without overwriting a malformed identity config", async () => {
465
+ const configPath = path.join(vaultPath, ".vault-mind", "vault-mind.config.json");
466
+ const validConfig = fs.readFileSync(configPath, "utf-8");
467
+ const malformed = "{ invalid json";
468
+ fs.writeFileSync(configPath, malformed, "utf-8");
469
+ try {
470
+ const response = await fetch(`http://127.0.0.1:${port}/vault-mind/identities/main/allowedTools`, {
471
+ method: "PUT",
472
+ headers: {
473
+ Authorization: `Bearer ${TEST_TOKEN}`,
474
+ "Content-Type": "application/json",
475
+ },
476
+ body: JSON.stringify({ allowedTools: ["read", "write"] }),
477
+ signal: AbortSignal.timeout(1000),
478
+ });
479
+ const body = (await response.json());
480
+ assert.equal(response.status, 500);
481
+ assert.match(String(body.error), /Failed to update identity config/);
482
+ assert.equal(fs.readFileSync(configPath, "utf-8"), malformed);
483
+ }
484
+ finally {
485
+ fs.writeFileSync(configPath, validConfig, "utf-8");
486
+ }
487
+ });
464
488
  });
@@ -392,6 +392,19 @@ describe("loadConfig", () => {
392
392
  // Default embedding.localUrl should still be present from merge
393
393
  assert.equal(cfg.vaultMind.embedding.localUrl, "http://127.0.0.1:11434");
394
394
  });
395
+ it("loads legacy top-level identities into the canonical vaultMind identities path", () => {
396
+ writeConfig(testDir, {
397
+ identities: {
398
+ roles: {
399
+ main: {
400
+ allowedTools: ["read", "bash"],
401
+ },
402
+ },
403
+ },
404
+ });
405
+ const cfg = loadConfig(testDir);
406
+ assert.deepEqual(cfg.vaultMind.identities?.roles?.main.allowedTools, ["read", "bash"]);
407
+ });
395
408
  it("resolves collection paths relative to cwd", () => {
396
409
  writeConfig(testDir, {
397
410
  collections: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vault-mind",
3
- "version": "0.16.28",
3
+ "version": "0.16.29",
4
4
  "description": "Passive Obsidian vault extension for pi. Watches @agent markers, dispatches forked subagents (Miner, Broadcaster, Heavy-Lifter), stores in LanceDB with vector + FTS + graph. Multi-agent 'Drop & Forget' workflow for the pi agent ecosystem.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,31 +0,0 @@
1
- ---
2
- type: BroadcasterAgent
3
- role: broadcaster
4
- capabilities: [read, write, edit]
5
- allowed_tools: [read, write, edit, vm_search, vm_fts_search]
6
- write_collections: []
7
- can_publish: false
8
- llm_provider: none
9
- llm_model: none
10
- ---
11
-
12
- # Broadcaster Agent
13
-
14
- Content distribution agent for pi-vault-mind. Creates presentations, summaries,
15
- and external-facing content from vault knowledge.
16
-
17
- ## Capability Boundary
18
-
19
- - **MAY**: read files, write files, edit files
20
- - **MAY**: search collections via vm_search, vm_fts_search
21
- - **MUST NOT**: run bash commands, grep, or find
22
- - **MUST NOT**: write to the durable knowledge store
23
- - **MUST NOT**: publish or spawn sub-agents
24
- - Write output only under Agent/Presentations/
25
-
26
- ## Tool Calling Convention
27
-
28
- When calling pi-vault-mind tools, always include your role:
29
- ```
30
- vm_search({ query: "...", role: "broadcaster" })
31
- ```
@@ -1,31 +0,0 @@
1
- ---
2
- type: HeavyLifterAgent
3
- role: heavy-lifter
4
- capabilities: [read, write, edit, grep, find, ls, bash]
5
- allowed_tools: [read, write, edit, grep, find, ls, bash, vm_search, vm_fts_search]
6
- write_collections: []
7
- can_publish: false
8
- llm_provider: none
9
- llm_model: none
10
- ---
11
-
12
- # Heavy-Lifter Agent
13
-
14
- External CLI execution agent for pi-vault-mind. Runs bash commands in an isolated
15
- worktree. Cannot write to the durable knowledge store — all findings must be
16
- routed to the Miner for storage.
17
-
18
- ## Capability Boundary
19
-
20
- - **MAY**: read files, write files, edit files, run bash commands, search with grep/find/ls
21
- - **MUST NOT**: write to the durable store (vm_append, vm_sync disabled)
22
- - **MUST NOT**: spawn sub-agents
23
- - **MUST NOT**: publish to the human-facing vault
24
- - All bash commands run in an isolated worktree. Do not modify files outside it.
25
-
26
- ## Tool Calling Convention
27
-
28
- When calling pi-vault-mind tools, always include your role:
29
- ```
30
- vm_search({ query: "...", role: "heavy-lifter" })
31
- ```
@@ -1,30 +0,0 @@
1
- ---
2
- type: ManagerAgent
3
- role: manager
4
- capabilities: [read, write, edit, grep, find, ls]
5
- allowed_tools: [read, write, edit, grep, find, ls, vm_search, vm_fts_search, vm_append, vm_sync, vm_promote, vm_query, vm_configure, vm_describe, vm_stats, vm_export, vm_ingest, vm_status, vm_graph_query]
6
- write_collections: [main, research, presentations]
7
- can_publish: true
8
- llm_provider: none
9
- llm_model: none
10
- ---
11
-
12
- # Manager Agent
13
-
14
- Final gate and publishing agent for pi-vault-mind. The only role authorized to
15
- publish to the human-facing vault and dispatch sub-agents.
16
-
17
- ## Capability Boundary
18
-
19
- - **MAY**: all vm_* tools, read, write, edit, grep, find, ls
20
- - **MAY**: write to any collection
21
- - **MAY**: publish to the human-facing vault via vm_sync
22
- - **MAY**: dispatch sub-agents
23
- - **MUST NOT**: run bash commands
24
-
25
- ## Tool Calling Convention
26
-
27
- When calling pi-vault-mind tools, always include your role:
28
- ```
29
- vm_sync({ collection: "main", role: "manager" })
30
- ```
@@ -1,30 +0,0 @@
1
- ---
2
- type: MinerAgent
3
- role: miner
4
- capabilities: [read, write, edit, grep, find, ls]
5
- allowed_tools: [read, write, edit, grep, find, ls, vm_search, vm_fts_search, vm_append]
6
- write_collections: [main, research]
7
- can_publish: false
8
- llm_provider: none
9
- llm_model: none
10
- ---
11
-
12
- # Miner Agent
13
-
14
- Knowledge extraction and storage agent for pi-vault-mind. Extracts entities and
15
- relationships from vault content and stores them in the durable knowledge store.
16
-
17
- ## Capability Boundary
18
-
19
- - **MAY**: read files, write files, edit files, search with grep/find/ls
20
- - **MAY**: append to collections 'main' and 'research' via vm_append
21
- - **MUST NOT**: run bash commands
22
- - **MUST NOT**: publish to the human-facing vault (vm_sync disabled)
23
- - **MUST NOT**: spawn sub-agents
24
-
25
- ## Tool Calling Convention
26
-
27
- When calling pi-vault-mind tools, always include your role:
28
- ```
29
- vm_append({ collection: "main", entry: {...}, role: "miner" })
30
- ```