@phi-code-admin/phi-code 0.63.0 → 0.63.2

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.
@@ -4,10 +4,10 @@
4
4
  * Now powered by sigma-memory package which provides:
5
5
  * - NotesManager: Markdown files management
6
6
  * - OntologyManager: Knowledge graph with entities and relations
7
- * - QMDManager: Vector search (if QMD is available)
7
+ * - VectorStore: Embedded vector search (sql.js + local embeddings)
8
8
  *
9
9
  * Features:
10
- * - memory_search: Unified search across notes, ontology, and QMD
10
+ * - memory_search: Unified search across notes, ontology, and vector store
11
11
  * - memory_write: Write content to memory files
12
12
  * - memory_read: Read specific memory files or list available ones
13
13
  * - memory_status: Get status of all memory subsystems
@@ -26,16 +26,12 @@ import { readFileSync } from "node:fs";
26
26
  import { SigmaMemory } from "sigma-memory";
27
27
 
28
28
  export default function memoryExtension(pi: ExtensionAPI) {
29
- // Initialize sigma-memory
30
- const sigmaMemory = new SigmaMemory({
31
- // Default configuration, can be overridden
32
- qmdEnabled: true,
33
- qmdCommand: 'qmd'
34
- });
29
+ // Initialize sigma-memory with embedded vector store
30
+ const sigmaMemory = new SigmaMemory();
35
31
 
36
- // Initialize memory directories
37
- sigmaMemory.init().catch(error => {
38
- console.warn("Failed to initialize sigma-memory:", error);
32
+ // Initialize memory + vector store (lazy model download on first search)
33
+ sigmaMemory.init().catch(() => {
34
+ // Non-critical memory works without vectors
39
35
  });
40
36
 
41
37
  /**
@@ -44,12 +40,15 @@ export default function memoryExtension(pi: ExtensionAPI) {
44
40
  pi.registerTool({
45
41
  name: "memory_search",
46
42
  label: "Memory Search",
47
- description: "Search for content in memory using unified search (notes + ontology + QMD vector search)",
43
+ description: "Search for content in memory using unified search (notes + ontology + vector search)",
48
44
  promptSnippet: "Search project memory (notes, ontology, vector search). ALWAYS call before answering questions about prior work, decisions, or project context.",
49
45
  promptGuidelines: [
50
46
  "Before answering questions about prior work, architecture, decisions, or project context: call memory_search first.",
51
47
  "When starting work on a topic, search memory for existing notes and learnings.",
52
48
  "After completing important work or learning something new, use memory_write to save it.",
49
+ "When a command fails or produces an unexpected error, document the error and fix in memory_write (self-improvement).",
50
+ "When the user corrects you, save the correction in memory_write so you never repeat the mistake.",
51
+ "After a significant debugging session, write a summary of root cause and solution to memory.",
53
52
  ],
54
53
  parameters: Type.Object({
55
54
  query: Type.String({ description: "Search query to find in memory" }),
@@ -96,7 +95,7 @@ export default function memoryExtension(pi: ExtensionAPI) {
96
95
  resultText += `Relation: ${data.type} (${data.from} → ${data.to})\n`;
97
96
  resultText += `Properties: ${JSON.stringify(data.properties)}\n\n`;
98
97
  }
99
- } else if (result.source === 'qmd') {
98
+ } else if (result.source === 'vectors') {
100
99
  const data = result.data;
101
100
  resultText += `File: ${data.file} (line ${data.line})\n`;
102
101
  resultText += `> ${data.content}\n\n`;
@@ -136,14 +135,18 @@ export default function memoryExtension(pi: ExtensionAPI) {
136
135
  const { content, file } = params as { content: string; file?: string };
137
136
 
138
137
  try {
139
- // Use notes manager to write
138
+ // Write to notes
140
139
  sigmaMemory.notes.write(content, file);
141
-
142
140
  const filename = file || new Date().toISOString().split('T')[0] + '.md';
141
+
142
+ // Auto-index in vector store (non-blocking)
143
+ sigmaMemory.vectors.addDocument(filename, content).catch(() => {
144
+ // Vector indexing failed silently — notes still saved
145
+ });
143
146
 
144
147
  return {
145
- content: [{ type: "text", text: `Content written to ${filename}` }],
146
- details: { filename, contentLength: content.length }
148
+ content: [{ type: "text", text: `Content written to ${filename} (indexed for vector search)` }],
149
+ details: { filename, contentLength: content.length, vectorIndexed: true }
147
150
  };
148
151
 
149
152
  } catch (error) {
@@ -214,7 +217,7 @@ export default function memoryExtension(pi: ExtensionAPI) {
214
217
  pi.registerTool({
215
218
  name: "memory_status",
216
219
  label: "Memory Status",
217
- description: "Get status of all memory subsystems (notes, ontology, QMD)",
220
+ description: "Get status of all memory subsystems (notes, ontology, vector search)",
218
221
  parameters: Type.Object({}),
219
222
 
220
223
  async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
@@ -236,14 +239,11 @@ export default function memoryExtension(pi: ExtensionAPI) {
236
239
  statusText += `- Entities by type: ${JSON.stringify(status.ontology.entitiesByType)}\n`;
237
240
  statusText += `- Relations by type: ${JSON.stringify(status.ontology.relationsByType)}\n\n`;
238
241
 
239
- // QMD status
240
- statusText += `## QMD Vector Search\n`;
241
- statusText += `- Available: ${status.qmd.available ? 'Yes' : 'No'}\n`;
242
- if (status.qmd.available && status.qmd.status) {
243
- statusText += `- Files indexed: ${status.qmd.status.files}\n`;
244
- statusText += `- Chunks: ${status.qmd.status.chunks}\n`;
245
- statusText += `- Last update: ${status.qmd.status.lastUpdate || 'Never'}\n`;
246
- }
242
+ // Vector store status
243
+ statusText += `## Vector Search (embedded)\n`;
244
+ statusText += `- Documents: ${status.vectors.documentCount}\n`;
245
+ statusText += `- Chunks: ${status.vectors.chunkCount}\n`;
246
+ statusText += `- Last update: ${status.vectors.lastUpdate || 'Never'}\n`;
247
247
 
248
248
  return {
249
249
  content: [{ type: "text", text: statusText }],
@@ -292,7 +292,7 @@ export default function memoryExtension(pi: ExtensionAPI) {
292
292
  const parts: string[] = [];
293
293
  if (status.notes.count > 0) parts.push(`${status.notes.count} notes`);
294
294
  if (status.ontology.entities > 0) parts.push(`${status.ontology.entities} entities`);
295
- if (status.qmd.available) parts.push("QMD active");
295
+ if (status.vectors.chunkCount > 0) parts.push(`${status.vectors.chunkCount} vectors`);
296
296
  if (parts.length > 0) {
297
297
  ctx.ui.notify(`🧠 Memory: ${parts.join(", ")}`, "info");
298
298
  }
@@ -450,23 +450,61 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
450
450
 
451
451
  // ─── Orchestration State ─────────────────────────────────────────
452
452
 
453
+ interface AgentDef {
454
+ name: string;
455
+ tools: string[];
456
+ systemPrompt: string;
457
+ }
458
+
453
459
  interface OrchestratorPhase {
454
460
  key: string;
455
461
  label: string;
456
462
  model: string;
457
463
  fallback: string;
464
+ agent: AgentDef | null;
458
465
  instruction: string;
459
466
  }
460
467
 
461
468
  let phaseQueue: OrchestratorPhase[] = [];
462
469
  let orchestrationActive = false;
463
470
  let idlePollTimer: ReturnType<typeof setInterval> | null = null;
471
+ let activeAgentPrompt: string | null = null;
472
+ let activeAgentTools: string[] | null = null;
473
+ let savedTools: string[] | null = null;
474
+
475
+ /**
476
+ * Parse agent .md file with YAML frontmatter
477
+ */
478
+ function loadAgentDef(name: string): AgentDef | null {
479
+ const dirs = [
480
+ join(process.cwd(), ".phi", "agents"),
481
+ join(homedir(), ".phi", "agent", "agents"),
482
+ ];
483
+ for (const dir of dirs) {
484
+ const filePath = join(dir, `${name}.md`);
485
+ try {
486
+ const content = readFileSync(filePath, "utf-8");
487
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
488
+ if (!fmMatch) continue;
489
+ const fields: Record<string, string> = {};
490
+ for (const line of fmMatch[1].split("\n")) {
491
+ const m = line.match(/^(\w+):\s*(.*)$/);
492
+ if (m) fields[m[1]] = m[2].trim();
493
+ }
494
+ return {
495
+ name: fields.name || name,
496
+ tools: (fields.tools || "").split(",").map(t => t.trim()).filter(Boolean),
497
+ systemPrompt: fmMatch[2].trim(),
498
+ };
499
+ } catch { continue; }
500
+ }
501
+ return null;
502
+ }
464
503
 
465
504
  /**
466
- * Load routing config and build phase queue with model assignments.
505
+ * Load routing config and build phase queue with model assignments + agent definitions.
467
506
  */
468
507
  function buildPhases(description: string): OrchestratorPhase[] {
469
- // Read routing.json for model assignments
470
508
  const routingPath = join(homedir(), ".phi", "agent", "routing.json");
471
509
  let routing: any = { routes: {}, default: { model: "default" } };
472
510
  try {
@@ -490,22 +528,27 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
490
528
  return [
491
529
  {
492
530
  key: "explore", label: "🔍 Phase 1 — EXPLORE", model: explore.preferred, fallback: explore.fallback,
531
+ agent: loadAgentDef("explore"),
493
532
  instruction: `Analyze the project requirements and existing codebase. Identify what exists, what's needed, and any constraints.\n\n**Project:** ${description}\n\nList files, read key ones, check dependencies. Return a structured summary.`,
494
533
  },
495
534
  {
496
535
  key: "plan", label: "📐 Phase 2 — PLAN", model: plan.preferred, fallback: plan.fallback,
536
+ agent: loadAgentDef("plan"),
497
537
  instruction: `Design the architecture for this project. Define file structure, tech choices, and implementation approach.\n\n**Project:** ${description}\n\nBe specific: list every file to create with its purpose.`,
498
538
  },
499
539
  {
500
540
  key: "code", label: "💻 Phase 3 — CODE", model: code.preferred, fallback: code.fallback,
541
+ agent: loadAgentDef("code"),
501
542
  instruction: `Implement the COMPLETE project. Create ALL files with production-quality code.\n\n**Project:** ${description}\n\n**Rules:**\n- Create every file needed\n- No placeholders, no TODOs, no stubs\n- Every function must be fully implemented\n- Follow the architecture from the previous planning phase`,
502
543
  },
503
544
  {
504
545
  key: "test", label: "🧪 Phase 4 — TEST", model: test.preferred, fallback: test.fallback,
546
+ agent: loadAgentDef("test"),
505
547
  instruction: `Test the implementation. Run the code, check for errors, verify it works.\n\n**Project:** ${description}\n\nFix any errors you find. Ensure the project runs correctly.`,
506
548
  },
507
549
  {
508
550
  key: "review", label: "🔍 Phase 5 — REVIEW", model: review.preferred, fallback: review.fallback,
551
+ agent: loadAgentDef("review"),
509
552
  instruction: `Review the code quality, security, and performance. Fix any issues.\n\n**Project:** ${description}\n\nCheck: error handling, edge cases, code style, documentation.`,
510
553
  },
511
554
  ];
@@ -513,7 +556,6 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
513
556
 
514
557
  /**
515
558
  * Switch model for the current phase.
516
- * Tries preferred model first, then fallback.
517
559
  */
518
560
  async function switchModelForPhase(phase: OrchestratorPhase, ctx: any): Promise<string> {
519
561
  const available = ctx.modelRegistry?.getAvailable?.() || [];
@@ -528,47 +570,90 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
528
570
  return ctx.model?.id || phase.model;
529
571
  }
530
572
 
573
+ /**
574
+ * Activate agent for a phase: set system prompt + restrict tools.
575
+ */
576
+ function activateAgent(phase: OrchestratorPhase, ctx: any) {
577
+ if (phase.agent) {
578
+ // Save current tools for restoration
579
+ if (!savedTools) {
580
+ savedTools = pi.getActiveTools();
581
+ }
582
+ // Set agent's system prompt (will be injected via before_agent_start)
583
+ activeAgentPrompt = phase.agent.systemPrompt;
584
+ // Restrict tools to agent's allowed tools
585
+ if (phase.agent.tools.length > 0) {
586
+ activeAgentTools = phase.agent.tools;
587
+ pi.setActiveTools(phase.agent.tools);
588
+ }
589
+ } else {
590
+ activeAgentPrompt = null;
591
+ activeAgentTools = null;
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Deactivate agent: restore tools, clear prompt override.
597
+ */
598
+ function deactivateAgent() {
599
+ activeAgentPrompt = null;
600
+ activeAgentTools = null;
601
+ if (savedTools) {
602
+ pi.setActiveTools(savedTools);
603
+ savedTools = null;
604
+ }
605
+ }
606
+
531
607
  /**
532
608
  * Send the next phase in the queue.
533
- * Called after the agent goes idle (previous phase complete).
534
609
  */
535
610
  function sendNextPhase(ctx: any) {
536
611
  if (phaseQueue.length === 0) {
537
- // All phases done
538
612
  orchestrationActive = false;
613
+ deactivateAgent();
539
614
  ctx.ui.notify(`\n✅ **All 5 phases complete!**`, "info");
540
615
  return;
541
616
  }
542
617
 
543
618
  const phase = phaseQueue.shift()!;
544
619
 
545
- // Switch model and send
546
620
  switchModelForPhase(phase, ctx).then((modelId) => {
547
- ctx.ui.notify(`\n${phase.label} → \`${modelId}\``, "info");
621
+ activateAgent(phase, ctx);
622
+ const agentName = phase.agent?.name || phase.key;
623
+ ctx.ui.notify(`\n${phase.label} → \`${modelId}\` (agent: ${agentName})`, "info");
548
624
  setTimeout(() => pi.sendUserMessage(phase.instruction), 200);
549
625
  });
550
626
  }
551
627
 
628
+ // ─── System Prompt Injection — Agent personas ────────────────────
629
+
630
+ pi.on("before_agent_start", async (event, _ctx) => {
631
+ if (!orchestrationActive || !activeAgentPrompt) {
632
+ return { };
633
+ }
634
+ // Replace system prompt with the active agent's prompt
635
+ return { systemPrompt: activeAgentPrompt };
636
+ });
637
+
552
638
  // ─── Output Event — Phase Chaining ───────────────────────────────
553
639
 
554
640
  pi.on("output", async (_event, ctx) => {
555
641
  if (!orchestrationActive || phaseQueue.length === 0) return;
556
642
 
557
- // Debounce: clear any existing poll timer
558
643
  if (idlePollTimer) {
559
644
  clearInterval(idlePollTimer);
560
645
  idlePollTimer = null;
561
646
  }
562
647
 
563
- // Poll for idle state (agent finishes tool calls + response)
564
648
  let attempts = 0;
565
649
  idlePollTimer = setInterval(() => {
566
650
  attempts++;
567
- if (attempts > 120) { // 60 seconds max
651
+ if (attempts > 120) {
568
652
  clearInterval(idlePollTimer!);
569
653
  idlePollTimer = null;
570
654
  ctx.ui.notify("⚠️ Orchestrator timeout — phase took too long.", "warning");
571
655
  orchestrationActive = false;
656
+ deactivateAgent();
572
657
  return;
573
658
  }
574
659
  if (ctx.isIdle()) {
@@ -605,23 +690,27 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
605
690
  const specFile = `spec-${ts}.md`;
606
691
  await writeFile(join(plansDir, specFile), `# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`, "utf-8");
607
692
 
608
- // Build phases with model assignments from routing.json
693
+ // Build phases with model assignments + agent definitions
609
694
  const phases = buildPhases(description);
610
695
  phaseQueue = phases.slice(1); // Queue phases 2-5
611
696
  orchestrationActive = true;
612
697
  const firstPhase = phases[0];
613
698
 
614
- ctx.ui.notify(`📋 **Orchestrator started** — 5 phases with model routing\n`, "info");
699
+ ctx.ui.notify(`📋 **Orchestrator started** — 5 phases with model routing + agent roles\n`, "info");
615
700
 
616
701
  // Show the plan
617
702
  for (const p of phases) {
618
- ctx.ui.notify(` ${p.label} \`${p.model}\``, "info");
703
+ const agentName = p.agent?.name || p.key;
704
+ const toolCount = p.agent?.tools.length || 0;
705
+ ctx.ui.notify(` ${p.label} → \`${p.model}\` (agent: ${agentName}, ${toolCount} tools)`, "info");
619
706
  }
620
707
  ctx.ui.notify("", "info");
621
708
 
622
- // Switch model and send first phase after handler returns
709
+ // Switch model and activate agent for first phase
623
710
  const modelId = await switchModelForPhase(firstPhase, ctx);
624
- ctx.ui.notify(`${firstPhase.label} → \`${modelId}\``, "info");
711
+ activateAgent(firstPhase, ctx);
712
+ const agentName = firstPhase.agent?.name || firstPhase.key;
713
+ ctx.ui.notify(`${firstPhase.label} → \`${modelId}\` (agent: ${agentName})`, "info");
625
714
  setTimeout(() => pi.sendUserMessage(firstPhase.instruction), 200);
626
715
  },
627
716
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.63.0",
3
+ "version": "0.63.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -61,7 +61,7 @@
61
61
  "phi-code-tui": "^0.56.3",
62
62
  "proper-lockfile": "^4.1.2",
63
63
  "sigma-agents": "^0.1.6",
64
- "sigma-memory": "^0.1.2",
64
+ "sigma-memory": "0.2.0",
65
65
  "sigma-skills": "^0.1.2",
66
66
  "strip-ansi": "^7.1.0",
67
67
  "undici": "^7.19.1",
@@ -58,7 +58,18 @@ for (const pkg of sigmaPackages) {
58
58
  createLink(srcPkg, destLink, pkg);
59
59
  }
60
60
 
61
- // 3. Ensure settings.json has quietStartup: true
61
+ // 3. Ensure memory directories exist (vector store DB created on first use)
62
+ const memoryDir = join(homedir(), ".phi", "memory");
63
+ const memoryNotesDir = join(memoryDir, "notes");
64
+ const memoryOntologyDir = join(memoryDir, "ontology");
65
+ for (const dir of [memoryDir, memoryNotesDir, memoryOntologyDir]) {
66
+ if (!existsSync(dir)) {
67
+ mkdirSync(dir, { recursive: true });
68
+ console.log(` Φ Created ${dir}`);
69
+ }
70
+ }
71
+
72
+ // 4. Ensure settings.json has quietStartup: true
62
73
  const settingsPath = join(agentDir, "settings.json");
63
74
  try {
64
75
  let settings = {};