@phi-code-admin/phi-code 0.86.0 → 0.88.0

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 (35) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/docs/adr/0001-phase-contract.md +57 -0
  3. package/docs/adr/0002-independent-review.md +47 -0
  4. package/docs/fork-policy.md +18 -0
  5. package/extensions/phi/agents.ts +7 -4
  6. package/extensions/phi/benchmark.ts +129 -49
  7. package/extensions/phi/browser.ts +10 -37
  8. package/extensions/phi/btw/btw.ts +1 -7
  9. package/extensions/phi/chrome/index.ts +1283 -741
  10. package/extensions/phi/commit.ts +9 -14
  11. package/extensions/phi/goal/index.ts +10 -33
  12. package/extensions/phi/init.ts +37 -47
  13. package/extensions/phi/keys.ts +2 -7
  14. package/extensions/phi/mcp/callback-server.ts +162 -165
  15. package/extensions/phi/mcp/config.ts +122 -136
  16. package/extensions/phi/mcp/errors.ts +18 -23
  17. package/extensions/phi/mcp/index.ts +322 -355
  18. package/extensions/phi/mcp/oauth-provider.ts +289 -289
  19. package/extensions/phi/mcp/server-manager.ts +390 -413
  20. package/extensions/phi/mcp/tool-bridge.ts +381 -415
  21. package/extensions/phi/memory.ts +2 -2
  22. package/extensions/phi/models.ts +27 -26
  23. package/extensions/phi/orchestrator.ts +451 -237
  24. package/extensions/phi/productivity.ts +4 -2
  25. package/extensions/phi/providers/agent-def.ts +1 -1
  26. package/extensions/phi/providers/alibaba.ts +56 -7
  27. package/extensions/phi/providers/context-window.ts +4 -1
  28. package/extensions/phi/providers/live-models.ts +5 -20
  29. package/extensions/phi/providers/orchestrator-helpers.ts +31 -5
  30. package/extensions/phi/providers/phase-machine.ts +283 -0
  31. package/extensions/phi/setup.ts +196 -169
  32. package/extensions/phi/skill-loader.ts +14 -17
  33. package/extensions/phi/smart-router.ts +6 -6
  34. package/extensions/phi/web-search.ts +90 -50
  35. package/package.json +1 -1
@@ -162,21 +162,22 @@ function deriveSubject(stagedFiles: string[], assistantHint: string): string {
162
162
 
163
163
  export default function commitExtension(pi: ExtensionAPI) {
164
164
  pi.registerCommand("commit", {
165
- description: "Deterministically commit staged changes ([phi] prefix, no LLM). Use --all to stage everything first.",
165
+ description:
166
+ "Deterministically commit staged changes ([phi] prefix, no LLM). Use --all to stage everything first.",
166
167
  handler: async (args: string, ctx: ExtensionCommandContext) => {
167
168
  try {
168
169
  // 1. Parse args: extract the --all flag, keep the rest as the message.
169
170
  const tokens = args.trim().split(/\s+/).filter(Boolean);
170
171
  const stageAll = tokens.includes("--all");
171
- const userMessage = tokens.filter((t) => t !== "--all").join(" ").trim();
172
+ const userMessage = tokens
173
+ .filter((t) => t !== "--all")
174
+ .join(" ")
175
+ .trim();
172
176
 
173
177
  // 2. Confirm we are inside a git repo with changes.
174
178
  const status = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
175
179
  if (status.code !== 0) {
176
- ctx.ui.notify(
177
- "Not a git repository (or git unavailable). Nothing to commit.",
178
- "warning",
179
- );
180
+ ctx.ui.notify("Not a git repository (or git unavailable). Nothing to commit.", "warning");
180
181
  return;
181
182
  }
182
183
  if (status.stdout.trim().length === 0) {
@@ -197,10 +198,7 @@ export default function commitExtension(pi: ExtensionAPI) {
197
198
  const afterStatus = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
198
199
  const stagedFiles = parseStagedFiles(afterStatus.stdout);
199
200
  if (stagedFiles.length === 0) {
200
- ctx.ui.notify(
201
- "No staged changes to commit. Stage files first, or run `/commit --all`.",
202
- "warning",
203
- );
201
+ ctx.ui.notify("No staged changes to commit. Stage files first, or run `/commit --all`.", "warning");
204
202
  return;
205
203
  }
206
204
 
@@ -243,10 +241,7 @@ export default function commitExtension(pi: ExtensionAPI) {
243
241
  "info",
244
242
  );
245
243
  } catch (err) {
246
- ctx.ui.notify(
247
- `/commit error: ${err instanceof Error ? err.message : String(err)}`,
248
- "error",
249
- );
244
+ ctx.ui.notify(`/commit error: ${err instanceof Error ? err.message : String(err)}`, "error");
250
245
  }
251
246
  },
252
247
  });
@@ -1,7 +1,6 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { dirname, join } from "node:path";
3
- import process from "node:process";
4
- import { randomUUID } from "node:crypto";
5
4
  import { defineTool, type ExtensionAPI, getAgentDir } from "phi-code";
6
5
  import { Type } from "typebox";
7
6
 
@@ -218,12 +217,7 @@ export default function goal(pi: ExtensionAPI) {
218
217
  });
219
218
  }
220
219
 
221
- async function startGoal(
222
- objective: string,
223
- tokenBudget: number | undefined,
224
- pi: ExtensionAPI,
225
- ctx: StatusContext,
226
- ) {
220
+ async function startGoal(objective: string, tokenBudget: number | undefined, pi: ExtensionAPI, ctx: StatusContext) {
227
221
  const validationError = validateObjective(objective);
228
222
  if (validationError) {
229
223
  ctx.ui.notify(validationError, "warning");
@@ -300,12 +294,7 @@ function clearGoal(ctx: StatusContext) {
300
294
  ctx.ui.notify(`Goal cleared: ${stoppedGoal}`, "warning");
301
295
  }
302
296
 
303
- async function editGoal(
304
- objective: string,
305
- tokenBudget: number | undefined,
306
- pi: ExtensionAPI,
307
- ctx: StatusContext,
308
- ) {
297
+ async function editGoal(objective: string, tokenBudget: number | undefined, pi: ExtensionAPI, ctx: StatusContext) {
309
298
  const validationError = validateObjective(objective);
310
299
  if (validationError) {
311
300
  ctx.ui.notify(validationError, "warning");
@@ -368,11 +357,7 @@ function editedGoalStatus(status: GoalStatus): GoalStatus {
368
357
  }
369
358
 
370
359
  function normalizeGoalForBudget(goal: ActiveGoal): ActiveGoal {
371
- if (
372
- goal.status === "active" &&
373
- goal.tokenBudget !== undefined &&
374
- goal.tokensUsed >= goal.tokenBudget
375
- ) {
360
+ if (goal.status === "active" && goal.tokenBudget !== undefined && goal.tokensUsed >= goal.tokenBudget) {
376
361
  return { ...goal, status: "budget_limited" };
377
362
  }
378
363
  return goal;
@@ -382,11 +367,7 @@ function incrementGoal(goal: ActiveGoal): ActiveGoal {
382
367
  return { ...goal, iteration: goal.iteration + 1, updatedAt: Date.now() };
383
368
  }
384
369
 
385
- function pauseGoalAfterAgentEnd(
386
- ctx: StatusContext,
387
- goal: ActiveGoal,
388
- assistant: AssistantMessageLike,
389
- ) {
370
+ function pauseGoalAfterAgentEnd(ctx: StatusContext, goal: ActiveGoal, assistant: AssistantMessageLike) {
390
371
  cancelContinuationPending();
391
372
  activeGoal = transitionGoal(goal, "paused");
392
373
  persistGoal(activeGoal);
@@ -582,7 +563,8 @@ function buildResumePrompt(goal: ActiveGoal) {
582
563
  }
583
564
 
584
565
  export function buildGoalSystemPrompt(goal: ActiveGoal) {
585
- const budgetLine = goal.tokenBudget === undefined ? "" : `\n- Respect the goal token budget (${formatBudget(goal)} used).`;
566
+ const budgetLine =
567
+ goal.tokenBudget === undefined ? "" : `\n- Respect the goal token budget (${formatBudget(goal)} used).`;
586
568
  return `Active /goal:\n${goalObjectiveBlock(goal)}\n\nGoal-mode rules:\n- Keep going until the active goal is completely resolved end-to-end.\n- Treat the current worktree, command output, tests, and external state as authoritative.\n- Do not redefine the goal into a smaller task; audit every requirement before completion.\n- Do not stop at analysis, a plan, TODO list, partial fixes, or suggested next steps.\n- Autonomously perform implementation and verification with the available tools when they are needed to complete the goal.\n- Persevere through recoverable tool failures by trying reasonable alternatives instead of yielding early.\n- If the goal is not complete at the end of a turn, expect an automatic continuation and keep working from where you left off.\n- Only call the goal_complete tool after the goal is fully complete and verified.${budgetLine}`;
587
569
  }
588
570
 
@@ -709,12 +691,10 @@ function loadGoalFromSession(ctx: StatusContext): ActiveGoal | undefined {
709
691
  | {
710
692
  getBranch?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
711
693
  getEntries?: () => Array<{ type?: string; customType?: string; data?: unknown }>;
712
- }
694
+ }
713
695
  | undefined;
714
696
  const entries = sessionManager?.getBranch?.() ?? sessionManager?.getEntries?.() ?? [];
715
- const entry = entries
716
- .filter((entry) => entry.type === "custom" && entry.customType === GOAL_STATE_ENTRY_TYPE)
717
- .pop();
697
+ const entry = entries.filter((entry) => entry.type === "custom" && entry.customType === GOAL_STATE_ENTRY_TYPE).pop();
718
698
  const data = entry?.data as GoalStateEntryData | undefined;
719
699
  return isGoal(data?.goal) && data.goal.status !== "complete" ? data.goal : undefined;
720
700
  }
@@ -750,9 +730,7 @@ function readState(): Record<string, unknown> {
750
730
  if (!existsSync(STATE_FILE)) return {};
751
731
  try {
752
732
  const parsed = JSON.parse(readFileSync(STATE_FILE, "utf8")) as unknown;
753
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
754
- ? (parsed as Record<string, unknown>)
755
- : {};
733
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : {};
756
734
  } catch {
757
735
  return {};
758
736
  }
@@ -766,7 +744,6 @@ function clearLegacyPersistedGoal(cwd: string) {
766
744
  writeFileSync(STATE_FILE, `${JSON.stringify(goals, null, 2)}\n`);
767
745
  }
768
746
 
769
-
770
747
  function isGoal(value: unknown): value is ActiveGoal {
771
748
  if (!value || typeof value !== "object") return false;
772
749
  const goal = value as Partial<ActiveGoal>;
@@ -17,11 +17,11 @@
17
17
  * so a network failure on /v1/models cannot lose the user's input.
18
18
  */
19
19
 
20
- import type { ExtensionAPI } from "phi-code";
21
- import { writeFile, mkdir, copyFile, readdir, readFile, chmod } from "node:fs/promises";
22
- import { join, resolve } from "node:path";
23
- import { homedir } from "node:os";
24
20
  import { existsSync } from "node:fs";
21
+ import { chmod, copyFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
22
+ import { homedir } from "node:os";
23
+ import { join, resolve } from "node:path";
24
+ import type { ExtensionAPI } from "phi-code";
25
25
  import { fetchLiveModels, pingProvider, toPersistedModel } from "./providers/live-models.js";
26
26
  import { isOpenCodeGoAnthropicModel, OPENCODE_GO_ANTHROPIC_BASE_URL } from "./providers/opencode-go.js";
27
27
 
@@ -218,12 +218,30 @@ function getAllAvailableModels(providers: DetectedProvider[]): Array<{ ref: stri
218
218
  // ─── Routing Presets ─────────────────────────────────────────────────────
219
219
 
220
220
  const TASK_ROLES = [
221
- { key: "code", label: "Code Generation", desc: "Writing and modifying code", agent: "code", defaultModel: "default" },
221
+ {
222
+ key: "code",
223
+ label: "Code Generation",
224
+ desc: "Writing and modifying code",
225
+ agent: "code",
226
+ defaultModel: "default",
227
+ },
222
228
  { key: "debug", label: "Debugging", desc: "Finding and fixing bugs", agent: "code", defaultModel: "default" },
223
229
  { key: "plan", label: "Planning", desc: "Architecture and design", agent: "plan", defaultModel: "default" },
224
- { key: "explore", label: "Exploration", desc: "Code reading and analysis", agent: "explore", defaultModel: "default" },
230
+ {
231
+ key: "explore",
232
+ label: "Exploration",
233
+ desc: "Code reading and analysis",
234
+ agent: "explore",
235
+ defaultModel: "default",
236
+ },
225
237
  { key: "test", label: "Testing", desc: "Running and writing tests", agent: "test", defaultModel: "default" },
226
- { key: "review", label: "Code Review", desc: "Quality and security review", agent: "review", defaultModel: "default" },
238
+ {
239
+ key: "review",
240
+ label: "Code Review",
241
+ desc: "Quality and security review",
242
+ agent: "review",
243
+ defaultModel: "default",
244
+ },
227
245
  ] as const;
228
246
 
229
247
  const KEYWORDS: Record<string, string[]> = {
@@ -235,9 +253,7 @@ const KEYWORDS: Record<string, string[]> = {
235
253
  review: ["review", "audit", "quality", "security", "improve", "optimize"],
236
254
  };
237
255
 
238
- function createRouting(
239
- assignments: Record<string, { preferred: string; fallback: string }>,
240
- ): RoutingConfig {
256
+ function createRouting(assignments: Record<string, { preferred: string; fallback: string }>): RoutingConfig {
241
257
  const routes: RoutingConfig["routes"] = {};
242
258
  for (const role of TASK_ROLES) {
243
259
  const assignment = assignments[role.key];
@@ -251,7 +267,7 @@ function createRouting(
251
267
  }
252
268
  return {
253
269
  routes,
254
- default: { model: assignments["default"]?.preferred || "default", agent: null },
270
+ default: { model: assignments.default?.preferred || "default", agent: null },
255
271
  };
256
272
  }
257
273
 
@@ -358,10 +374,7 @@ _Edit this file to customize Phi Code's behavior for your project._
358
374
  }
359
375
  }
360
376
 
361
- async function persistProviderKey(
362
- provider: DetectedProvider,
363
- apiKey: string,
364
- ): Promise<void> {
377
+ async function persistProviderKey(provider: DetectedProvider, apiKey: string): Promise<void> {
365
378
  const config = await readModelsConfig();
366
379
  const existing = config.providers[provider.id] ?? {};
367
380
  config.providers[provider.id] = {
@@ -448,7 +461,7 @@ _Edit this file to customize Phi Code's behavior for your project._
448
461
 
449
462
  // Orchestrator fallback (used only when a specific route has no model).
450
463
  // This is NOT the chat default — `/model` controls that.
451
- assignments["default"] = {
464
+ assignments.default = {
452
465
  preferred: "default",
453
466
  fallback: availableModels[0]?.ref || "default",
454
467
  };
@@ -464,10 +477,7 @@ _Edit this file to customize Phi Code's behavior for your project._
464
477
  if (result.source === "live" && result.models.length > 0) {
465
478
  provider.models = result.models.map((m) => m.id);
466
479
  provider.available = true;
467
- ctx.ui.notify(
468
- `${provider.name} is running with ${provider.models.length} model(s).\n`,
469
- "info",
470
- );
480
+ ctx.ui.notify(`${provider.name} is running with ${provider.models.length} model(s).\n`, "info");
471
481
  } else {
472
482
  ctx.ui.notify(
473
483
  `${provider.name} not reachable on port ${port}. Start it and re-run \`/phi-init\`.\n`,
@@ -483,10 +493,7 @@ _Edit this file to customize Phi Code's behavior for your project._
483
493
  "info",
484
494
  );
485
495
 
486
- const apiKey = await ctx.ui.input(
487
- `Enter your ${provider.name} API key`,
488
- "Paste your key here",
489
- );
496
+ const apiKey = await ctx.ui.input(`Enter your ${provider.name} API key`, "Paste your key here");
490
497
 
491
498
  if (apiKey === undefined) {
492
499
  ctx.ui.notify("Cancelled. No key saved.", "warning");
@@ -632,10 +639,7 @@ _Edit this file to customize Phi Code's behavior for your project._
632
639
  return `${status} ${p.name}${tag}${modelCount}`;
633
640
  }),
634
641
  ];
635
- const addProvider = await ctx.ui.select(
636
- "Configure a provider (add multiple!):",
637
- providerOptions,
638
- );
642
+ const addProvider = await ctx.ui.select("Configure a provider (add multiple!):", providerOptions);
639
643
 
640
644
  const choiceIdx = providerOptions.indexOf(addProvider ?? "");
641
645
  if (choiceIdx <= 0) {
@@ -657,18 +661,12 @@ _Edit this file to customize Phi Code's behavior for your project._
657
661
 
658
662
  const available = providers.filter((p) => p.available);
659
663
  if (available.length === 0) {
660
- ctx.ui.notify(
661
- "No providers available. Run `/phi-init` again after setting up a provider.",
662
- "error",
663
- );
664
+ ctx.ui.notify("No providers available. Run `/phi-init` again after setting up a provider.", "error");
664
665
  return;
665
666
  }
666
667
 
667
668
  const allModels = getAllAvailableModels(providers);
668
- ctx.ui.notify(
669
- `\n${allModels.length} models available from ${available.length} provider(s).\n`,
670
- "info",
671
- );
669
+ ctx.ui.notify(`\n${allModels.length} models available from ${available.length} provider(s).\n`, "info");
672
670
 
673
671
  // 2. Assign models to agents
674
672
  ctx.ui.notify("Assign a model to each agent role:\n", "info");
@@ -680,11 +678,7 @@ _Edit this file to customize Phi Code's behavior for your project._
680
678
 
681
679
  ctx.ui.notify("Writing routing configuration...", "info");
682
680
  const routing = createRouting(assignments);
683
- await writeFile(
684
- join(agentDir, "routing.json"),
685
- JSON.stringify(routing, null, 2),
686
- "utf-8",
687
- );
681
+ await writeFile(join(agentDir, "routing.json"), JSON.stringify(routing, null, 2), "utf-8");
688
682
 
689
683
  ctx.ui.notify("Setting up sub-agents...", "info");
690
684
  await copyBundledAgents();
@@ -702,10 +696,7 @@ _Edit this file to customize Phi Code's behavior for your project._
702
696
  const a = assignments[role.key];
703
697
  ctx.ui.notify(` ${role.label}: \`${a.preferred}\` (fallback: \`${a.fallback}\`)`, "info");
704
698
  }
705
- ctx.ui.notify(
706
- "\nChat default model: use `/model` (this wizard does NOT change the chat default).",
707
- "info",
708
- );
699
+ ctx.ui.notify("\nChat default model: use `/model` (this wizard does NOT change the chat default).", "info");
709
700
  ctx.ui.notify("\nNext steps:", "info");
710
701
  ctx.ui.notify(" - `/model` to pick the chat default model (sticky across prompts)", "info");
711
702
  ctx.ui.notify(" - `/plan <description>` to run the orchestrator with the roles above", "info");
@@ -729,8 +720,7 @@ _Edit this file to customize Phi Code's behavior for your project._
729
720
  description: "Reconfigure the per-role models used by /plan (provider-qualified, cross-provider)",
730
721
  handler: async (_args, ctx) => {
731
722
  try {
732
- const registryModels: Array<{ provider?: string; id?: string }> =
733
- ctx.modelRegistry?.getAvailable?.() || [];
723
+ const registryModels: Array<{ provider?: string; id?: string }> = ctx.modelRegistry?.getAvailable?.() || [];
734
724
  const available: Array<{ ref: string; display: string }> = [];
735
725
  const seen = new Set<string>();
736
726
  for (const m of registryModels) {
@@ -22,9 +22,7 @@
22
22
 
23
23
  import { ApiKeyStore, type ExtensionAPI, getApiKeyStore, getConfigWatcher } from "phi-code";
24
24
 
25
- interface ProviderPingFn {
26
- (key: string, timeoutMs?: number): Promise<{ ok: boolean; error?: string }>;
27
- }
25
+ type ProviderPingFn = (key: string, timeoutMs?: number) => Promise<{ ok: boolean; error?: string }>;
28
26
 
29
27
  const PROVIDER_PING_REGISTRY: Record<string, ProviderPingFn> = {};
30
28
 
@@ -152,10 +150,7 @@ export default function keysExtension(pi: ExtensionAPI) {
152
150
  return;
153
151
  }
154
152
 
155
- ctx.ui.notify(
156
- "Unknown subcommand. Use: `/keys [list|set|remove|test|reload]`",
157
- "warning",
158
- );
153
+ ctx.ui.notify("Unknown subcommand. Use: `/keys [list|set|remove|test|reload]`", "warning");
159
154
  } catch (err) {
160
155
  ctx.ui.notify(`/keys error: ${err instanceof Error ? err.message : String(err)}`, "error");
161
156
  }