@phi-code-admin/phi-code 0.62.2 → 0.63.1

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.
@@ -2,21 +2,18 @@
2
2
  * Orchestrator Extension - Full-cycle project planning and execution
3
3
  *
4
4
  * WORKFLOW (single command):
5
- * /plan <description> → LLM analyzes orchestrate tool spec + todo auto-execute → progress
6
- * Everything happens in one shot. No manual steps.
5
+ * /plan <description> → 5 sequential agent phaseseach with its own model
6
+ *
7
+ * The orchestrator uses event-driven phase chaining:
8
+ * 1. Send phase 1 message with model A
9
+ * 2. Detect when agent goes idle (output event + polling)
10
+ * 3. Switch to model B, send phase 2
11
+ * 4. Repeat until all 5 phases complete
7
12
  *
8
13
  * Commands:
9
- * /plan — Full workflow: plan + execute with sub-agents
10
- * /run — Re-execute an existing plan (e.g. after fixes)
14
+ * /plan — Full workflow: plan + execute with agents
15
+ * /run — Re-execute an existing plan
11
16
  * /plans — List plans and their execution status
12
- *
13
- * Sub-agent execution:
14
- * Each task spawns a separate `phi` CLI process with:
15
- * - Its own system prompt (from the agent .md file)
16
- * - Its own model (from routing.json)
17
- * - Its own context (isolated, no shared history)
18
- * - Its own tool access (read, write, edit, bash, etc.)
19
- * Results are collected into progress.md and reported to the user.
20
17
  */
21
18
 
22
19
  import { Type } from "@sinclair/typebox";
@@ -451,10 +448,226 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
451
448
  },
452
449
  });
453
450
 
451
+ // ─── Orchestration State ─────────────────────────────────────────
452
+
453
+ interface AgentDef {
454
+ name: string;
455
+ tools: string[];
456
+ systemPrompt: string;
457
+ }
458
+
459
+ interface OrchestratorPhase {
460
+ key: string;
461
+ label: string;
462
+ model: string;
463
+ fallback: string;
464
+ agent: AgentDef | null;
465
+ instruction: string;
466
+ }
467
+
468
+ let phaseQueue: OrchestratorPhase[] = [];
469
+ let orchestrationActive = false;
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
+ }
503
+
504
+ /**
505
+ * Load routing config and build phase queue with model assignments + agent definitions.
506
+ */
507
+ function buildPhases(description: string): OrchestratorPhase[] {
508
+ const routingPath = join(homedir(), ".phi", "agent", "routing.json");
509
+ let routing: any = { routes: {}, default: { model: "default" } };
510
+ try {
511
+ routing = JSON.parse(readFileSync(routingPath, "utf-8"));
512
+ } catch { /* no routing config */ }
513
+
514
+ function getModel(routeKey: string): { preferred: string; fallback: string } {
515
+ const route = routing.routes?.[routeKey];
516
+ return {
517
+ preferred: route?.preferredModel || routing.default?.model || "default",
518
+ fallback: route?.fallback || routing.default?.model || "default",
519
+ };
520
+ }
521
+
522
+ const explore = getModel("explore");
523
+ const plan = getModel("plan");
524
+ const code = getModel("code");
525
+ const test = getModel("test");
526
+ const review = getModel("review");
527
+
528
+ return [
529
+ {
530
+ key: "explore", label: "🔍 Phase 1 — EXPLORE", model: explore.preferred, fallback: explore.fallback,
531
+ agent: loadAgentDef("explore"),
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.`,
533
+ },
534
+ {
535
+ key: "plan", label: "📐 Phase 2 — PLAN", model: plan.preferred, fallback: plan.fallback,
536
+ agent: loadAgentDef("plan"),
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.`,
538
+ },
539
+ {
540
+ key: "code", label: "💻 Phase 3 — CODE", model: code.preferred, fallback: code.fallback,
541
+ agent: loadAgentDef("code"),
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`,
543
+ },
544
+ {
545
+ key: "test", label: "🧪 Phase 4 — TEST", model: test.preferred, fallback: test.fallback,
546
+ agent: loadAgentDef("test"),
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.`,
548
+ },
549
+ {
550
+ key: "review", label: "🔍 Phase 5 — REVIEW", model: review.preferred, fallback: review.fallback,
551
+ agent: loadAgentDef("review"),
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.`,
553
+ },
554
+ ];
555
+ }
556
+
557
+ /**
558
+ * Switch model for the current phase.
559
+ */
560
+ async function switchModelForPhase(phase: OrchestratorPhase, ctx: any): Promise<string> {
561
+ const available = ctx.modelRegistry?.getAvailable?.() || [];
562
+ const preferred = available.find((m: any) => m.id === phase.model);
563
+ const fallback = available.find((m: any) => m.id === phase.fallback);
564
+ const target = preferred || fallback;
565
+
566
+ if (target && target.id !== ctx.model?.id) {
567
+ const switched = await pi.setModel(target);
568
+ if (switched) return target.id;
569
+ }
570
+ return ctx.model?.id || phase.model;
571
+ }
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
+
607
+ /**
608
+ * Send the next phase in the queue.
609
+ */
610
+ function sendNextPhase(ctx: any) {
611
+ if (phaseQueue.length === 0) {
612
+ orchestrationActive = false;
613
+ deactivateAgent();
614
+ ctx.ui.notify(`\n✅ **All 5 phases complete!**`, "info");
615
+ return;
616
+ }
617
+
618
+ const phase = phaseQueue.shift()!;
619
+
620
+ switchModelForPhase(phase, ctx).then((modelId) => {
621
+ activateAgent(phase, ctx);
622
+ const agentName = phase.agent?.name || phase.key;
623
+ ctx.ui.notify(`\n${phase.label} → \`${modelId}\` (agent: ${agentName})`, "info");
624
+ setTimeout(() => pi.sendUserMessage(phase.instruction), 200);
625
+ });
626
+ }
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
+
638
+ // ─── Output Event — Phase Chaining ───────────────────────────────
639
+
640
+ pi.on("output", async (_event, ctx) => {
641
+ if (!orchestrationActive || phaseQueue.length === 0) return;
642
+
643
+ if (idlePollTimer) {
644
+ clearInterval(idlePollTimer);
645
+ idlePollTimer = null;
646
+ }
647
+
648
+ let attempts = 0;
649
+ idlePollTimer = setInterval(() => {
650
+ attempts++;
651
+ if (attempts > 120) {
652
+ clearInterval(idlePollTimer!);
653
+ idlePollTimer = null;
654
+ ctx.ui.notify("⚠️ Orchestrator timeout — phase took too long.", "warning");
655
+ orchestrationActive = false;
656
+ deactivateAgent();
657
+ return;
658
+ }
659
+ if (ctx.isIdle()) {
660
+ clearInterval(idlePollTimer!);
661
+ idlePollTimer = null;
662
+ sendNextPhase(ctx);
663
+ }
664
+ }, 500);
665
+ });
666
+
454
667
  // ─── /plan Command — Full workflow ───────────────────────────────
455
668
 
456
669
  pi.registerCommand("plan", {
457
- description: "Plan AND execute a project with agents describe what to build",
670
+ description: "Plan AND execute a project — 5 phases, each with its own model from routing.json",
458
671
  handler: async (args, ctx) => {
459
672
  const description = args.trim();
460
673
 
@@ -477,36 +690,28 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
477
690
  const specFile = `spec-${ts}.md`;
478
691
  await writeFile(join(plansDir, specFile), `# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`, "utf-8");
479
692
 
480
- ctx.ui.notify(`📋 Launching orchestrator...`, "info");
481
-
482
- // Build a single comprehensive prompt with all agent phases
483
- const prompt = `# Project: ${description}
484
-
485
- ## Your workflow (follow these phases in order):
486
-
487
- ### Phase 1 — 🔍 EXPLORE
488
- Analyze the project requirements and any existing codebase. List what exists, what's needed, and constraints.
489
-
490
- ### Phase 2 — 📐 PLAN
491
- Design the architecture, file structure, and tech choices. Be specific about file names and structure.
693
+ // Build phases with model assignments + agent definitions
694
+ const phases = buildPhases(description);
695
+ phaseQueue = phases.slice(1); // Queue phases 2-5
696
+ orchestrationActive = true;
697
+ const firstPhase = phases[0];
492
698
 
493
- ### Phase 3💻 CODE
494
- Implement EVERYTHING. Create ALL files with complete, production-quality code. No placeholders, no TODOs, no "implement later". Every file must be fully functional.
699
+ ctx.ui.notify(`📋 **Orchestrator started**5 phases with model routing + agent roles\n`, "info");
495
700
 
496
- ### Phase 4 — 🧪 TEST
497
- Run the code. Verify it works. Fix any errors you find.
498
-
499
- ### Phase 5 🔍 REVIEW
500
- Check code quality. Fix any issues. Ensure everything is polished.
501
-
502
- ## Rules
503
- - Complete ALL 5 phases in this turn
504
- - Create every file needed for a working project
505
- - Announce each phase as you start it (e.g. "## Phase 1 — Exploring...")
506
- - Do NOT stop after planning — implement everything`;
507
-
508
- // Send after handler returns (agent goes idle after command completes)
509
- setTimeout(() => pi.sendUserMessage(prompt), 200);
701
+ // Show the plan
702
+ for (const p of phases) {
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");
706
+ }
707
+ ctx.ui.notify("", "info");
708
+
709
+ // Switch model and activate agent for first phase
710
+ const modelId = await switchModelForPhase(firstPhase, ctx);
711
+ activateAgent(firstPhase, ctx);
712
+ const agentName = firstPhase.agent?.name || firstPhase.key;
713
+ ctx.ui.notify(`${firstPhase.label} \`${modelId}\` (agent: ${agentName})`, "info");
714
+ setTimeout(() => pi.sendUserMessage(firstPhase.instruction), 200);
510
715
  },
511
716
  });
512
717
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phi-code-admin/phi-code",
3
- "version": "0.62.2",
3
+ "version": "0.63.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {