pi-vault-mind 0.8.0 → 0.8.3

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 (119) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +44 -44
  3. package/agents/broadcaster.agent.md +30 -0
  4. package/agents/heavy-lifter.agent.md +30 -0
  5. package/agents/manager.agent.md +29 -0
  6. package/agents/miner.agent.md +29 -0
  7. package/dist/src/agent-bus/Agent.d.ts +49 -0
  8. package/dist/src/agent-bus/Agent.js +97 -0
  9. package/dist/src/agent-bus/AgentLoader.d.ts +33 -0
  10. package/dist/src/agent-bus/AgentLoader.js +102 -0
  11. package/dist/src/agent-bus/AgentRegistry.d.ts +17 -0
  12. package/dist/src/agent-bus/AgentRegistry.js +20 -0
  13. package/dist/src/agent-bus/LLMProvider.d.ts +36 -0
  14. package/dist/src/agent-bus/LLMProvider.js +125 -0
  15. package/dist/src/agent-bus/MessageBus.d.ts +47 -0
  16. package/dist/src/agent-bus/MessageBus.js +81 -0
  17. package/dist/src/agent-bus/TaskQueue.d.ts +27 -0
  18. package/dist/src/agent-bus/TaskQueue.js +57 -0
  19. package/dist/src/agent-bus/index.d.ts +6 -0
  20. package/dist/src/agent-bus/index.js +6 -0
  21. package/dist/src/agents/BroadcasterAgent.d.ts +19 -0
  22. package/dist/src/agents/BroadcasterAgent.js +70 -0
  23. package/dist/src/agents/HeavyLifterAgent.d.ts +26 -0
  24. package/dist/src/agents/HeavyLifterAgent.js +73 -0
  25. package/dist/src/agents/ManagerAgent.d.ts +20 -0
  26. package/dist/src/agents/ManagerAgent.js +71 -0
  27. package/dist/src/agents/MinerAgent.d.ts +19 -0
  28. package/dist/src/agents/MinerAgent.js +70 -0
  29. package/dist/src/agents/index.d.ts +4 -0
  30. package/dist/src/agents/index.js +4 -0
  31. package/dist/src/auth.d.ts +49 -2
  32. package/dist/src/auth.js +149 -7
  33. package/dist/src/autosync.d.ts +1 -1
  34. package/dist/src/autosync.js +3 -3
  35. package/dist/src/bridge.d.ts +45 -0
  36. package/dist/src/bridge.js +189 -0
  37. package/dist/src/commands.d.ts +1 -1
  38. package/dist/src/commands.js +419 -295
  39. package/dist/src/context-capture.d.ts +22 -0
  40. package/dist/src/context-capture.js +41 -0
  41. package/dist/src/discover-schema.d.ts +1 -1
  42. package/dist/src/discover-schema.js +4 -4
  43. package/dist/src/embed-queue.d.ts +1 -1
  44. package/dist/src/embed-queue.js +1 -1
  45. package/dist/src/engine.d.ts +65 -0
  46. package/dist/src/engine.js +244 -0
  47. package/dist/src/events.js +1 -1
  48. package/dist/src/graph.d.ts +3 -3
  49. package/dist/src/identity-injector.d.ts +15 -0
  50. package/dist/src/identity-injector.js +102 -0
  51. package/dist/src/index.js +88 -49
  52. package/dist/src/intake.js +4 -4
  53. package/dist/src/lance.d.ts +11 -37
  54. package/dist/src/lance.js +183 -240
  55. package/dist/src/modal-client.d.ts +14 -4
  56. package/dist/src/modal-client.js +50 -34
  57. package/dist/src/modal-config.d.ts +26 -10
  58. package/dist/src/modal-config.js +36 -5
  59. package/dist/src/personalize.d.ts +47 -0
  60. package/dist/src/personalize.js +352 -0
  61. package/dist/src/server.d.ts +30 -4
  62. package/dist/src/server.js +146 -64
  63. package/dist/src/settings-ui.d.ts +3 -1
  64. package/dist/src/settings-ui.js +84 -77
  65. package/dist/src/status.d.ts +25 -0
  66. package/dist/src/status.js +49 -0
  67. package/dist/src/sync.d.ts +1 -1
  68. package/dist/src/sync.js +19 -19
  69. package/dist/src/tombstone.js +2 -2
  70. package/dist/src/tools.js +112 -130
  71. package/dist/src/types.d.ts +86 -18
  72. package/dist/src/types.js +7 -4
  73. package/dist/src/utils.d.ts +5 -1
  74. package/dist/src/utils.js +24 -8
  75. package/dist/src/vault-tools.d.ts +29 -0
  76. package/dist/src/vault-tools.js +314 -0
  77. package/dist/src/vault-writer.d.ts +4 -5
  78. package/dist/src/vault-writer.js +13 -13
  79. package/dist/src/vm-handlers.d.ts +15 -0
  80. package/dist/src/vm-handlers.js +469 -0
  81. package/dist/src/watcher.d.ts +18 -4
  82. package/dist/src/watcher.js +109 -98
  83. package/dist/test/auth.test.js +57 -7
  84. package/dist/test/bridge-identity.test.js +388 -0
  85. package/dist/test/discover-schema.test.js +2 -2
  86. package/dist/test/engine-identity.test.js +119 -0
  87. package/dist/test/index.test.js +5 -6
  88. package/dist/test/lance-modal.test.js +7 -7
  89. package/dist/test/modal-config.test.js +51 -29
  90. package/dist/test/personalize.test.js +212 -0
  91. package/dist/test/rest-queue.test.js +1 -1
  92. package/dist/test/rest-setup.test.js +21 -21
  93. package/dist/test/{rest-wiki.test.js → rest-vm.test.js} +27 -23
  94. package/dist/test/server-ws.test.js +4 -4
  95. package/dist/test/sync.test.js +2 -2
  96. package/dist/test/tombstone.test.js +1 -1
  97. package/dist/test/vault-tools.test.js +224 -0
  98. package/package.json +2 -1
  99. package/scripts/check-deps.sh +207 -0
  100. package/scripts/e2e-ws.mjs +4 -4
  101. package/scripts/modal-e2e-smoke.mjs +1 -1
  102. package/scripts/setup-test-config.sh +1 -5
  103. package/scripts/test-slash-commands.mjs +197 -0
  104. package/skills/.agents/broadcaster.agent.md +31 -0
  105. package/skills/.agents/heavy-lifter.agent.md +31 -0
  106. package/skills/.agents/manager.agent.md +30 -0
  107. package/skills/.agents/miner.agent.md +30 -0
  108. package/skills/vault-mind/SKILL.md +50 -50
  109. package/skills/vault-mind/references/tool-reference.md +25 -25
  110. package/skills/vault-mind-broadcaster/SKILL.md +1 -1
  111. package/skills/vault-mind-heavy-lifter/SKILL.md +3 -3
  112. package/skills/vault-mind-manager/SKILL.md +7 -7
  113. package/skills/vault-mind-miner/SKILL.md +11 -11
  114. package/skills/vault-mind-setup/SKILL.md +65 -265
  115. package/skills/vault-mind-setup/references/obsidian-vault-structure.md +6 -6
  116. package/skills/vault-mind-setup/references/pi-extension-wiring.md +24 -33
  117. package/skills/vault-mind-setup/references/troubleshooting-tree.md +25 -26
  118. package/dist/src/auto-index.d.ts +0 -47
  119. package/dist/src/auto-index.js +0 -122
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Context Capture — Layer 4 auto-trigger pi-context /acm.
3
+ *
4
+ * On extension load, auto-sends /acm to enable agentic context management.
5
+ * Injects context_checkpoint instructions into agent task prompts.
6
+ *
7
+ * Note: context_compact requires CommandCtx (only in main session, not
8
+ * subagent forks). Subagent forks use context_checkpoint and context_timeline.
9
+ */
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ /**
12
+ * Auto-send /acm on extension load to enable agentic context management
13
+ * for the Manager session.
14
+ */
15
+ export declare function enableACM(pi: ExtensionAPI): void;
16
+ /**
17
+ * Build context_checkpoint instructions to inject into a dispatch task prompt.
18
+ *
19
+ * Checkpoint naming convention: vault-mind-{role}-{dispatchId}-{phase}
20
+ * for audit traceability.
21
+ */
22
+ export declare function buildCheckpointInstructions(role: string, dispatchId: string): string;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Context Capture — Layer 4 auto-trigger pi-context /acm.
3
+ *
4
+ * On extension load, auto-sends /acm to enable agentic context management.
5
+ * Injects context_checkpoint instructions into agent task prompts.
6
+ *
7
+ * Note: context_compact requires CommandCtx (only in main session, not
8
+ * subagent forks). Subagent forks use context_checkpoint and context_timeline.
9
+ */
10
+ // ── ACM auto-trigger ──────────────────────────────────────────────────────────
11
+ /**
12
+ * Auto-send /acm on extension load to enable agentic context management
13
+ * for the Manager session.
14
+ */
15
+ export function enableACM(pi) {
16
+ pi.sendUserMessage("/acm Enable agentic context management for this vault-mind session. " +
17
+ "Use context_checkpoint to mark major dispatch milestones. " +
18
+ "Use context_timeline to review progress. " +
19
+ "Use context_compact when context is full.", { deliverAs: "followUp" });
20
+ }
21
+ // ── Checkpoint instructions for agent task prompts ────────────────────────────
22
+ /**
23
+ * Build context_checkpoint instructions to inject into a dispatch task prompt.
24
+ *
25
+ * Checkpoint naming convention: vault-mind-{role}-{dispatchId}-{phase}
26
+ * for audit traceability.
27
+ */
28
+ export function buildCheckpointInstructions(role, dispatchId) {
29
+ return [
30
+ "## Context Management",
31
+ "Use context_checkpoint to mark major milestones in your work:",
32
+ "",
33
+ `- Start: context_checkpoint({ name: "vault-mind-${role}-${dispatchId}-start" })`,
34
+ `- After reading source: context_checkpoint({ name: "vault-mind-${role}-${dispatchId}-read" })`,
35
+ `- After executing tools: context_checkpoint({ name: "vault-mind-${role}-${dispatchId}-execute" })`,
36
+ `- Before writeback: context_checkpoint({ name: "vault-mind-${role}-${dispatchId}-writeback" })`,
37
+ "",
38
+ "Use context_timeline to review your progress at any point.",
39
+ "These checkpoints create an audit trail for the Agent/Journal/.",
40
+ ].join("\n");
41
+ }
@@ -23,5 +23,5 @@ export declare const writeCollectionToConfig: (cwd: string, name: string, def: C
23
23
  export declare const runDiscoverSchema: (cwd: string, file: string, name?: string, sample?: number) => DiscoverSchemaResult;
24
24
  /** Register the `discover_schema` Pi tool. */
25
25
  export declare const registerDiscoverSchemaTool: (pi: ExtensionAPI) => void;
26
- /** Handler for the /wiki discover-schema slash command. */
26
+ /** Handler for the /vm discover-schema slash command. */
27
27
  export declare const handleDiscoverSchema: (args: string, ctx: ExtensionContext) => Promise<void>;
@@ -148,7 +148,7 @@ export const registerDiscoverSchemaTool = (pi) => {
148
148
  pi.registerTool({
149
149
  name: "discover_schema",
150
150
  label: "Discover JSONL Schema",
151
- description: "Infer the schema of an unconfigured .jsonl file by sampling its first N rows and register it as a wiki collection.",
151
+ description: "Infer the schema of an unconfigured .jsonl file by sampling its first N rows and register it as a collection.",
152
152
  promptSnippet: 'discover_schema(file="data/events.jsonl", name="events", sample=100)',
153
153
  promptGuidelines: [
154
154
  "Use when you have a .jsonl file that is not yet mapped in pi-vault-mind.config.json.",
@@ -184,11 +184,11 @@ export const registerDiscoverSchemaTool = (pi) => {
184
184
  },
185
185
  });
186
186
  };
187
- /** Parse CLI-style arguments for /wiki discover-schema. */
187
+ /** Parse CLI-style arguments for /vm discover-schema. */
188
188
  const parseDiscoverSchemaArgs = (args) => {
189
189
  const tokens = args.trim().split(/\s+/).filter(Boolean);
190
190
  if (tokens.length === 0) {
191
- return { error: "Usage: /wiki discover-schema <file> [--name <collection>] [--sample <n>]" };
191
+ return { error: "Usage: /vm discover-schema <file> [--name <collection>] [--sample <n>]" };
192
192
  }
193
193
  let file;
194
194
  let name;
@@ -223,7 +223,7 @@ const parseDiscoverSchemaArgs = (args) => {
223
223
  }
224
224
  return { file, name, sample };
225
225
  };
226
- /** Handler for the /wiki discover-schema slash command. */
226
+ /** Handler for the /vm discover-schema slash command. */
227
227
  export const handleDiscoverSchema = async (args, ctx) => {
228
228
  const parsed = parseDiscoverSchemaArgs(args);
229
229
  if (parsed.error) {
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * ROUTING POLICY (yours to own, not baked in here):
26
26
  * - append / ingest / bulk → coalescer.embed(text, "document") [debounced]
27
- * - interactive wiki_search → coalescer.embedImmediate(query, "query")
27
+ * - interactive vm_search → coalescer.embedImmediate(query, "query")
28
28
  * (latency-sensitive; bypasses the debounce)
29
29
  *
30
30
  * It is provider-agnostic: `embedFn` can wrap modal, ollama, or transformers —
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * ROUTING POLICY (yours to own, not baked in here):
26
26
  * - append / ingest / bulk → coalescer.embed(text, "document") [debounced]
27
- * - interactive wiki_search → coalescer.embedImmediate(query, "query")
27
+ * - interactive vm_search → coalescer.embedImmediate(query, "query")
28
28
  * (latency-sensitive; bypasses the debounce)
29
29
  *
30
30
  * It is provider-agnostic: `embedFn` can wrap modal, ollama, or transformers —
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Vault Mind Agent Engine — agent lifecycle manager.
3
+ *
4
+ * Adapted from microfactory's engine loop pattern. On extension load:
5
+ * 1. Discover .agent.md files via AgentLoader
6
+ * 2. Instantiate agents, register with MessageBus
7
+ * 3. Register agent identities with the bridge for tool gating
8
+ * 4. Run agent tick() loops concurrently
9
+ *
10
+ * Vendored patterns from kylebrodeur/pi-agent-bus and kylebrodeur/microfactory (MIT).
11
+ */
12
+ import type { MessageBus } from "./agent-bus/MessageBus.js";
13
+ import type { TaskQueue } from "./agent-bus/TaskQueue.js";
14
+ import { type AgentIdentity } from "./bridge.js";
15
+ import type { IdentityConfig } from "./types.js";
16
+ /** Register all vault-mind agent types. Call once at extension load. */
17
+ export declare function registerAgentTypes(): void;
18
+ export interface EngineConfig {
19
+ /**
20
+ * Extra directory containing user .agent.md files (e.g. <vault>/.agents/).
21
+ * Bundled defaults from EXT_ROOT/agents/ are always loaded first.
22
+ * User files with the same id override bundled defaults.
23
+ * Optional — safe to omit.
24
+ */
25
+ agentsDir?: string;
26
+ /** Shared MessageBus for agent coordination. */
27
+ bus: MessageBus;
28
+ /** Shared TaskQueue for work distribution. */
29
+ taskQueue: TaskQueue;
30
+ }
31
+ export interface EngineState {
32
+ bus: MessageBus;
33
+ taskQueue: TaskQueue;
34
+ agents: import("./agent-bus/Agent.js").Agent[];
35
+ running: boolean;
36
+ }
37
+ /**
38
+ * Create and start the agent engine.
39
+ *
40
+ * Discovers .agent.md files, instantiates agents, registers identities
41
+ * with the bridge, and begins the tick loop.
42
+ */
43
+ export declare function startEngine(config: EngineConfig): Promise<EngineState>;
44
+ /** Stop the engine. */
45
+ export declare function stopEngine(state: EngineState): void;
46
+ /**
47
+ * Hard-coded last-resort identity profile for a role. Returns the baseline
48
+ * allowlist / write-scope / publish-bit used when no `identities` block is
49
+ * present in config. See P0 in `docs/plans/agent-identity-model.md`.
50
+ */
51
+ export declare function buildIdentityFromRole(role: string, id: string): AgentIdentity;
52
+ /**
53
+ * Merge identity layers (later wins): `base` ← `defaultProfile` ← `roleProfile`.
54
+ *
55
+ * Merges `allowedTools`, `writeCollections`, `canPublish`, `allowedVaultPaths`,
56
+ * `readCollections`, and `egressAllowlist`.
57
+ * Other `IdentityConfig` fields (`tokenRef`) are accepted in config for
58
+ * forward-compat but not applied here.
59
+ *
60
+ * Fields that are `undefined` in a layer are left untouched, so a config can
61
+ * override `writeCollections` alone without having to restate the full
62
+ * allowlist. `id` and `role` always come from `base` — config cannot rename
63
+ * an agent.
64
+ */
65
+ export declare function mergeIdentityProfiles(base: AgentIdentity, defaultProfile: IdentityConfig | undefined, roleProfile: IdentityConfig | undefined): AgentIdentity;
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Vault Mind Agent Engine — agent lifecycle manager.
3
+ *
4
+ * Adapted from microfactory's engine loop pattern. On extension load:
5
+ * 1. Discover .agent.md files via AgentLoader
6
+ * 2. Instantiate agents, register with MessageBus
7
+ * 3. Register agent identities with the bridge for tool gating
8
+ * 4. Run agent tick() loops concurrently
9
+ *
10
+ * Vendored patterns from kylebrodeur/pi-agent-bus and kylebrodeur/microfactory (MIT).
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { AgentLoader } from "./agent-bus/AgentLoader.js";
15
+ import { AgentRegistry } from "./agent-bus/AgentRegistry.js";
16
+ import { AgentModelProvider } from "./agent-bus/LLMProvider.js";
17
+ import { BroadcasterAgent, HeavyLifterAgent, ManagerAgent, MinerAgent } from "./agents/index.js";
18
+ import { registerAgentIdentity } from "./bridge.js";
19
+ import { EXT_ROOT, loadConfig, resolveVaultFolder } from "./utils.js";
20
+ // ── Agent registry setup ─────────────────────────────────────────────────────
21
+ /** Register all vault-mind agent types. Call once at extension load. */
22
+ export function registerAgentTypes() {
23
+ AgentRegistry.register("HeavyLifterAgent", HeavyLifterAgent);
24
+ AgentRegistry.register("MinerAgent", MinerAgent);
25
+ AgentRegistry.register("BroadcasterAgent", BroadcasterAgent);
26
+ AgentRegistry.register("ManagerAgent", ManagerAgent);
27
+ }
28
+ /**
29
+ * Create and start the agent engine.
30
+ *
31
+ * Discovers .agent.md files, instantiates agents, registers identities
32
+ * with the bridge, and begins the tick loop.
33
+ */
34
+ export async function startEngine(config) {
35
+ registerAgentTypes();
36
+ // Load config early — needed by llmProviderFactory for model/baseUrl defaults
37
+ const cfg = loadConfig(process.cwd());
38
+ const llmProviderFactory = async (_provider, _model) => {
39
+ // Use AgentModelProvider for real LLM access. Model comes from the agent's
40
+ // .agent.md frontmatter (llm_model field) or falls back to the agentLLM
41
+ // config — vault-mind agents need a reasoning model, not a dummy.
42
+ const agentLLM = cfg.vaultMind.agentLLM ?? {};
43
+ const model = _model && _model !== "none" ? _model : agentLLM.model;
44
+ const remoteUrl = agentLLM.remoteUrl || "http://127.0.0.1:11434/v1";
45
+ return new AgentModelProvider({
46
+ model,
47
+ remoteUrl,
48
+ localUrl: agentLLM.localUrl,
49
+ temperature: 0.1,
50
+ });
51
+ };
52
+ // 1. Always load bundled defaults from EXT_ROOT/agents/
53
+ const bundledDir = path.join(EXT_ROOT, "agents");
54
+ const bundledLoader = new AgentLoader({
55
+ agentsDirectory: bundledDir,
56
+ bus: config.bus,
57
+ llmProviderFactory,
58
+ });
59
+ const bundledAgents = await bundledLoader.loadAgents();
60
+ const agentMap = new Map(bundledAgents.map((a) => [a.config.id, a]));
61
+ // 2. Optionally load user overrides from <vault>/.agents/
62
+ const userDir = config.agentsDir ?? path.join(process.cwd(), ".agents");
63
+ if (fs.existsSync(userDir)) {
64
+ const userLoader = new AgentLoader({
65
+ agentsDirectory: userDir,
66
+ bus: config.bus,
67
+ llmProviderFactory,
68
+ });
69
+ const userAgents = await userLoader.loadAgents();
70
+ // User files with the same id override bundled defaults
71
+ for (const agent of userAgents) {
72
+ agentMap.set(agent.config.id, agent);
73
+ }
74
+ }
75
+ const agents = Array.from(agentMap.values());
76
+ // Set task queue on each agent (not passed via constructor — AgentLoader
77
+ // creates agents with (config, bus, llm?) signature)
78
+ for (const agent of agents) {
79
+ if ("setTaskQueue" in agent) {
80
+ agent.setTaskQueue(config.taskQueue);
81
+ }
82
+ }
83
+ // Register each agent's identity with the bridge for tool gating.
84
+ // Per-role identity profiles can be overridden via the user's vault config
85
+ // (`identities.defaultProfile` baseline + `identities.roles[role]` per-role);
86
+ // absent config means the hard-coded `buildIdentityFromRole` map applies.
87
+ const identitiesCfg = cfg.vaultMind.identities ?? {};
88
+ const presentationsFolder = resolveVaultFolder(cfg.vaultMind, "presentations");
89
+ for (const agent of agents) {
90
+ const base = buildIdentityFromRole(agent.config.role, agent.config.id);
91
+ if (base.role === "broadcaster") {
92
+ base.allowedVaultPaths = [`${presentationsFolder}/`];
93
+ }
94
+ const identity = mergeIdentityProfiles(base, identitiesCfg.defaultProfile, identitiesCfg.roles?.[agent.config.role]);
95
+ registerAgentIdentity(identity);
96
+ }
97
+ // Start tick loop — agents are event-driven, tick is a no-op heartbeat
98
+ const state = {
99
+ bus: config.bus,
100
+ taskQueue: config.taskQueue,
101
+ agents,
102
+ running: true,
103
+ };
104
+ // Heartbeat: periodically tick all agents
105
+ const tickInterval = setInterval(() => {
106
+ if (!state.running) {
107
+ clearInterval(tickInterval);
108
+ return;
109
+ }
110
+ for (const agent of state.agents) {
111
+ void agent.tick().catch((err) => {
112
+ console.error(`[Engine] Agent ${agent.config.id} tick error:`, err);
113
+ });
114
+ }
115
+ }, 5000);
116
+ return state;
117
+ }
118
+ /** Stop the engine. */
119
+ export function stopEngine(state) {
120
+ state.running = false;
121
+ }
122
+ // ── Identity builder ─────────────────────────────────────────────────────────
123
+ /**
124
+ * Hard-coded last-resort identity profile for a role. Returns the baseline
125
+ * allowlist / write-scope / publish-bit used when no `identities` block is
126
+ * present in config. See P0 in `docs/plans/agent-identity-model.md`.
127
+ */
128
+ export function buildIdentityFromRole(role, id) {
129
+ switch (role) {
130
+ case "heavy-lifter":
131
+ return {
132
+ id,
133
+ role,
134
+ allowedTools: [
135
+ "read",
136
+ "write",
137
+ "edit",
138
+ "grep",
139
+ "find",
140
+ "ls",
141
+ "bash",
142
+ "vm_search",
143
+ "vm_fts_search",
144
+ ],
145
+ writeCollections: [],
146
+ canPublish: false,
147
+ };
148
+ case "miner":
149
+ return {
150
+ id,
151
+ role,
152
+ allowedTools: [
153
+ "read",
154
+ "write",
155
+ "edit",
156
+ "grep",
157
+ "find",
158
+ "ls",
159
+ "vm_search",
160
+ "vm_fts_search",
161
+ "vm_append",
162
+ ],
163
+ writeCollections: ["main", "research"],
164
+ canPublish: false,
165
+ };
166
+ case "broadcaster":
167
+ return {
168
+ id,
169
+ role,
170
+ allowedTools: ["read", "write", "edit", "vm_search", "vm_fts_search"],
171
+ writeCollections: [],
172
+ canPublish: false,
173
+ };
174
+ case "manager":
175
+ return {
176
+ id,
177
+ role,
178
+ allowedTools: [
179
+ "read",
180
+ "write",
181
+ "edit",
182
+ "grep",
183
+ "find",
184
+ "ls",
185
+ "vm_search",
186
+ "vm_fts_search",
187
+ "vm_append",
188
+ "vm_sync",
189
+ "vm_promote",
190
+ "vm_query",
191
+ "vm_configure",
192
+ "vm_describe",
193
+ "vm_stats",
194
+ "vm_export",
195
+ "vm_ingest",
196
+ "vm_status",
197
+ "vm_graph_query",
198
+ ],
199
+ writeCollections: ["main", "research", "presentations"],
200
+ canPublish: true,
201
+ };
202
+ default:
203
+ return {
204
+ id,
205
+ role,
206
+ allowedTools: ["read", "vm_search", "vm_fts_search"],
207
+ writeCollections: [],
208
+ canPublish: false,
209
+ };
210
+ }
211
+ }
212
+ /**
213
+ * Merge identity layers (later wins): `base` ← `defaultProfile` ← `roleProfile`.
214
+ *
215
+ * Merges `allowedTools`, `writeCollections`, `canPublish`, `allowedVaultPaths`,
216
+ * `readCollections`, and `egressAllowlist`.
217
+ * Other `IdentityConfig` fields (`tokenRef`) are accepted in config for
218
+ * forward-compat but not applied here.
219
+ *
220
+ * Fields that are `undefined` in a layer are left untouched, so a config can
221
+ * override `writeCollections` alone without having to restate the full
222
+ * allowlist. `id` and `role` always come from `base` — config cannot rename
223
+ * an agent.
224
+ */
225
+ export function mergeIdentityProfiles(base, defaultProfile, roleProfile) {
226
+ const merged = { ...base };
227
+ for (const layer of [defaultProfile, roleProfile]) {
228
+ if (!layer)
229
+ continue;
230
+ if (layer.allowedTools !== undefined)
231
+ merged.allowedTools = layer.allowedTools;
232
+ if (layer.writeCollections !== undefined)
233
+ merged.writeCollections = layer.writeCollections;
234
+ if (layer.canPublish !== undefined)
235
+ merged.canPublish = layer.canPublish;
236
+ if (layer.allowedVaultPaths !== undefined)
237
+ merged.allowedVaultPaths = layer.allowedVaultPaths;
238
+ if (layer.egressAllowlist !== undefined)
239
+ merged.egressAllowlist = layer.egressAllowlist;
240
+ if (layer.readCollections !== undefined)
241
+ merged.readCollections = layer.readCollections;
242
+ }
243
+ return merged;
244
+ }
@@ -8,7 +8,7 @@ import { ensureDir, getPiContextConfig, hasPiContextTools, loadConfig } from "./
8
8
  export const handleBeforeAgentStart = async (pi, event, ctx) => {
9
9
  const cfg = loadConfig(ctx.cwd);
10
10
  const activeCollection = getActiveCollection(ctx.cwd);
11
- let additions = `\n\n[Active Collection Context]\nThe currently active collection is "${activeCollection}". When using append_wiki or running queries, prefer this collection as the target unless specified otherwise.\n`;
11
+ let additions = `\n\n[Active Collection Context]\nThe currently active collection is "${activeCollection}". When using vm_append or running queries, prefer this collection as the target unless specified otherwise.\n`;
12
12
  // 1. Standard injector matching
13
13
  for (const ij of cfg.injectors) {
14
14
  const regex = new RegExp(ij.regex, "i");
@@ -1,3 +1,3 @@
1
- import type { WikiConfig } from "./types.js";
2
- export declare const graphUpsert: (dataDir: string, cfg: WikiConfig, entry: Record<string, string>) => Promise<void>;
3
- export declare const queryGraph: (dataDir: string, cfg: WikiConfig, entityQuery: string, depth: number) => Promise<unknown>;
1
+ import type { VaultMindConfig } from "./types.js";
2
+ export declare const graphUpsert: (dataDir: string, cfg: VaultMindConfig, entry: Record<string, string>) => Promise<void>;
3
+ export declare const queryGraph: (dataDir: string, cfg: VaultMindConfig, entityQuery: string, depth: number) => Promise<unknown>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Identity Injector — Layer 3 system prompt boundary contract.
3
+ *
4
+ * Hooks pi.on("before_agent_start") to inject a hardened system prompt
5
+ * per role stating capability boundaries as a contract. Pattern adapted
6
+ * from pi-guideline-loader (kylebrodeur/pi-guideline-loader, MIT).
7
+ */
8
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
+ /**
10
+ * Register the identity injector on a pi ExtensionAPI.
11
+ *
12
+ * On every before_agent_start event, detects the current role and injects
13
+ * the corresponding boundary prompt into the system prompt.
14
+ */
15
+ export declare function registerIdentityInjector(pi: ExtensionAPI): void;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Identity Injector — Layer 3 system prompt boundary contract.
3
+ *
4
+ * Hooks pi.on("before_agent_start") to inject a hardened system prompt
5
+ * per role stating capability boundaries as a contract. Pattern adapted
6
+ * from pi-guideline-loader (kylebrodeur/pi-guideline-loader, MIT).
7
+ */
8
+ // ── Per-role boundary prompts ────────────────────────────────────────────────
9
+ const BOUNDARY_PROMPTS = {
10
+ "heavy-lifter": [
11
+ "--- IDENTITY BOUNDARY: Heavy-Lifter ---",
12
+ "You are the Heavy-Lifter agent. Your capability boundary:",
13
+ "- You MAY: read files, write files, edit files, run bash commands, search with grep/find/ls.",
14
+ "- You MUST NOT: write to the durable knowledge store (vm_append, vm_sync are disabled).",
15
+ " Route all findings to the Miner for storage.",
16
+ "- You MUST NOT: spawn sub-agents.",
17
+ "- You MUST NOT: publish to the human-facing vault.",
18
+ "- All bash commands run in an isolated worktree. Do not modify files outside it.",
19
+ "------------------------------------------",
20
+ ].join("\n"),
21
+ miner: [
22
+ "--- IDENTITY BOUNDARY: Miner ---",
23
+ "You are the Miner agent. Your capability boundary:",
24
+ "- You MAY: read files, write files, edit files, search with grep/find/ls.",
25
+ "- You MAY: append to collections 'main' and 'research' via vm_append.",
26
+ "- You MUST NOT: run bash commands.",
27
+ "- You MUST NOT: publish to the human-facing vault (vm_sync is disabled).",
28
+ "- You MUST NOT: spawn sub-agents.",
29
+ "------------------------------------------",
30
+ ].join("\n"),
31
+ broadcaster: [
32
+ "--- IDENTITY BOUNDARY: Broadcaster ---",
33
+ "You are the Broadcaster agent. Your capability boundary:",
34
+ "- You MAY: read files, write files, edit files.",
35
+ "- You MAY: search collections via vm_search, vm_fts_search.",
36
+ "- You MUST NOT: run bash commands, grep, or find.",
37
+ "- You MUST NOT: write to the durable knowledge store.",
38
+ "- You MUST NOT: publish or spawn sub-agents.",
39
+ "- Write output only under Agent/Presentations/.",
40
+ "------------------------------------------",
41
+ ].join("\n"),
42
+ manager: [
43
+ "--- IDENTITY BOUNDARY: Manager ---",
44
+ "You are the Manager agent. Your capability boundary:",
45
+ "- You MAY: all vm_* tools, read, write, edit, grep, find, ls.",
46
+ "- You MAY: write to any collection.",
47
+ "- You MAY: publish to the human-facing vault via vm_sync.",
48
+ "- You MAY: dispatch sub-agents.",
49
+ "- You MUST NOT: run bash commands.",
50
+ "------------------------------------------",
51
+ ].join("\n"),
52
+ };
53
+ // ── Role detection ────────────────────────────────────────────────────────────
54
+ /**
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.
65
+ *
66
+ * Returns undefined for any non-vault-mind session; no injection occurs.
67
+ */
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
+ }
77
+ }
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();
82
+ }
83
+ // ── Injector ──────────────────────────────────────────────────────────────────
84
+ /**
85
+ * Register the identity injector on a pi ExtensionAPI.
86
+ *
87
+ * On every before_agent_start event, detects the current role and injects
88
+ * the corresponding boundary prompt into the system prompt.
89
+ */
90
+ export function registerIdentityInjector(pi) {
91
+ pi.on("before_agent_start", async (event) => {
92
+ const role = detectRole(event);
93
+ if (!role)
94
+ return;
95
+ const boundaryPrompt = BOUNDARY_PROMPTS[role];
96
+ if (!boundaryPrompt)
97
+ return;
98
+ return {
99
+ systemPrompt: `${event.systemPrompt}\n\n${boundaryPrompt}`,
100
+ };
101
+ });
102
+ }