pi-teams 0.9.13 → 0.9.14

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.
@@ -554,7 +554,7 @@ export default function (pi: ExtensionAPI) {
554
554
  prompt: Type.String(),
555
555
  cwd: Type.String(),
556
556
  model: Type.Optional(Type.String()),
557
- thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high"])),
557
+ thinking: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"])),
558
558
  plan_mode_required: Type.Optional(Type.Boolean({ default: false })),
559
559
  separate_window: Type.Optional(Type.Boolean({ default: false })),
560
560
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-teams",
3
- "version": "0.9.13",
3
+ "version": "0.9.14",
4
4
  "description": "Agent teams for pi, ported from claude-code-teams-mcp",
5
5
  "repository": {
6
6
  "type": "git",
@@ -62,12 +62,16 @@ describe("CmuxAdapter", () => {
62
62
  process.env.CMUX_SOCKET_PATH = "/tmp/cmux.sock";
63
63
  });
64
64
 
65
- it("should spawn a new pane and return the surface ID", () => {
66
- mockExecCommand.mockReturnValue({
67
- stdout: "OK surface-42",
68
- stderr: "",
69
- status: 0
70
- });
65
+ it("should spawn via new-split + poll + respawn-pane and return the new surface", () => {
66
+ mockExecCommand
67
+ // 1. listSurfaceRefs (before snapshot) — realistic cmux output
68
+ .mockReturnValueOnce({ stdout: "* surface:1 Terminal [selected]\n surface:2 Terminal\n", stderr: "", status: 0 })
69
+ // 2. new-split right
70
+ .mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 })
71
+ // 3. listSurfaceRefs (poll — finds new surface:42)
72
+ .mockReturnValueOnce({ stdout: "* surface:1 Terminal [selected]\n surface:2 Terminal\n surface:42 Terminal\n", stderr: "", status: 0 })
73
+ // 4. respawn-pane
74
+ .mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 });
71
75
 
72
76
  const result = adapter.spawn({
73
77
  name: "test-agent",
@@ -76,19 +80,25 @@ describe("CmuxAdapter", () => {
76
80
  env: { PI_AGENT_ID: "test-123" },
77
81
  });
78
82
 
79
- expect(result).toBe("surface-42");
80
- expect(mockExecCommand).toHaveBeenCalledWith(
81
- "cmux",
82
- ["new-split", "right", "--command", "env PI_AGENT_ID=test-123 pi --agent test"]
83
- );
83
+ expect(result).toBe("surface:42");
84
+
85
+ // Verify new-split was called without --command
86
+ expect(mockExecCommand).toHaveBeenCalledWith("cmux", ["new-split", "right"]);
87
+
88
+ // Verify respawn-pane was called with the new surface and full command
89
+ expect(mockExecCommand).toHaveBeenCalledWith("cmux", [
90
+ "respawn-pane",
91
+ "--surface", "surface:42",
92
+ "--command", "env PI_AGENT_ID=test-123 pi --agent test",
93
+ ]);
84
94
  });
85
95
 
86
96
  it("should spawn without env prefix when no PI_ vars", () => {
87
- mockExecCommand.mockReturnValue({
88
- stdout: "OK surface-99",
89
- stderr: "",
90
- status: 0
91
- });
97
+ mockExecCommand
98
+ .mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
99
+ .mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 })
100
+ .mockReturnValueOnce({ stdout: " surface:1 Terminal\n surface:99 Terminal\n", stderr: "", status: 0 })
101
+ .mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 });
92
102
 
93
103
  const result = adapter.spawn({
94
104
  name: "test-agent",
@@ -97,19 +107,20 @@ describe("CmuxAdapter", () => {
97
107
  env: { OTHER: "ignored" },
98
108
  });
99
109
 
100
- expect(result).toBe("surface-99");
101
- expect(mockExecCommand).toHaveBeenCalledWith(
102
- "cmux",
103
- ["new-split", "right", "--command", "pi"]
104
- );
110
+ expect(result).toBe("surface:99");
111
+ expect(mockExecCommand).toHaveBeenCalledWith("cmux", [
112
+ "respawn-pane",
113
+ "--surface", "surface:99",
114
+ "--command", "pi",
115
+ ]);
105
116
  });
106
117
 
107
- it("should throw on spawn failure", () => {
108
- mockExecCommand.mockReturnValue({
109
- stdout: "",
110
- stderr: "cmux not found",
111
- status: 1
112
- });
118
+ it("should throw on new-split failure", () => {
119
+ mockExecCommand
120
+ // listSurfaceRefs (before)
121
+ .mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
122
+ // new-split fails
123
+ .mockReturnValueOnce({ stdout: "", stderr: "cmux not found", status: 1 });
113
124
 
114
125
  expect(() => adapter.spawn({
115
126
  name: "test-agent",
@@ -119,19 +130,43 @@ describe("CmuxAdapter", () => {
119
130
  })).toThrow("cmux new-split failed with status 1");
120
131
  });
121
132
 
122
- it("should throw on unexpected output format", () => {
123
- mockExecCommand.mockReturnValue({
124
- stdout: "ERROR something went wrong",
125
- stderr: "",
126
- status: 0
127
- });
133
+ it("should throw when new surface is not found after polling", () => {
134
+ mockExecCommand
135
+ // listSurfaceRefs (before)
136
+ .mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
137
+ // new-split OK
138
+ .mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 });
139
+
140
+ // All subsequent polls return the same surfaces (no new one appears)
141
+ // Each poll cycle = listSurfaceRefs + sleep
142
+ for (let i = 0; i < 20; i++) {
143
+ mockExecCommand
144
+ .mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 }) // listSurfaceRefs
145
+ .mockReturnValueOnce({ stdout: "", stderr: "", status: 0 }); // sleep
146
+ }
147
+
148
+ expect(() => adapter.spawn({
149
+ name: "test-agent",
150
+ cwd: "/home/user/project",
151
+ command: "pi",
152
+ env: {},
153
+ })).toThrow("new surface was not found");
154
+ });
155
+
156
+ it("should throw when respawn-pane fails", () => {
157
+ mockExecCommand
158
+ .mockReturnValueOnce({ stdout: " surface:1 Terminal\n", stderr: "", status: 0 })
159
+ .mockReturnValueOnce({ stdout: "OK", stderr: "", status: 0 })
160
+ .mockReturnValueOnce({ stdout: " surface:1 Terminal\n surface:50 Terminal\n", stderr: "", status: 0 })
161
+ // respawn-pane fails
162
+ .mockReturnValueOnce({ stdout: "", stderr: "respawn error", status: 1 });
128
163
 
129
164
  expect(() => adapter.spawn({
130
165
  name: "test-agent",
131
166
  cwd: "/home/user/project",
132
167
  command: "pi",
133
168
  env: {},
134
- })).toThrow("cmux new-split returned unexpected output");
169
+ })).toThrow("cmux respawn-pane failed with status 1");
135
170
  });
136
171
  });
137
172
 
@@ -306,4 +341,4 @@ describe("CmuxAdapter", () => {
306
341
  expect(adapter.isWindowAlive(undefined as unknown as string)).toBe(false);
307
342
  });
308
343
  });
309
- });
344
+ });
@@ -2,10 +2,20 @@
2
2
  * CMUX Terminal Adapter
3
3
  *
4
4
  * Implements the TerminalAdapter interface for CMUX (cmux.dev).
5
+ *
6
+ * Spawn strategy: cmux's `new-split` does not support a `--command` flag.
7
+ * We follow the proven pattern from pi-cmux (npm:pi-cmux):
8
+ * 1. Snapshot existing surfaces
9
+ * 2. `cmux new-split <direction>`
10
+ * 3. Poll `cmux list-pane-surfaces` to find the newly created surface
11
+ * 4. `cmux respawn-pane --surface <id> --command <cmd>` to run the command
5
12
  */
6
13
 
7
14
  import { TerminalAdapter, SpawnOptions, execCommand } from "../utils/terminal-adapter";
8
15
 
16
+ const SURFACE_POLL_ATTEMPTS = 20;
17
+ const SURFACE_POLL_DELAY_MS = 150;
18
+
9
19
  export class CmuxAdapter implements TerminalAdapter {
10
20
  readonly name = "cmux";
11
21
 
@@ -18,34 +28,79 @@ export class CmuxAdapter implements TerminalAdapter {
18
28
  return !!process.env.CMUX_SOCKET_PATH || !!process.env.CMUX_WORKSPACE_ID;
19
29
  }
20
30
 
31
+ /**
32
+ * List all surface refs currently visible in the workspace.
33
+ */
34
+ private listSurfaceRefs(): Set<string> {
35
+ const refs = new Set<string>();
36
+ try {
37
+ const result = execCommand("cmux", ["list-pane-surfaces"]);
38
+ if (result.status === 0) {
39
+ for (const line of result.stdout.split("\n")) {
40
+ // Output lines look like: "* surface:5 ⠹ π · ziahmco [selected]"
41
+ // Extract the surface:N ref from each line.
42
+ const match = line.match(/\b(surface:\d+)\b/);
43
+ if (match) refs.add(match[1]);
44
+ }
45
+ }
46
+ } catch {
47
+ // Ignore
48
+ }
49
+ return refs;
50
+ }
51
+
52
+ /**
53
+ * Block until a new surface appears that was not in `before`, or give up.
54
+ */
55
+ private waitForNewSurface(before: Set<string>): string | null {
56
+ for (let i = 0; i < SURFACE_POLL_ATTEMPTS; i++) {
57
+ const current = this.listSurfaceRefs();
58
+ for (const ref of current) {
59
+ if (!before.has(ref)) return ref;
60
+ }
61
+ // spawnSync-based sleep — keeps the adapter synchronous
62
+ execCommand("sleep", [String(SURFACE_POLL_DELAY_MS / 1000)]);
63
+ }
64
+ return null;
65
+ }
66
+
21
67
  spawn(options: SpawnOptions): string {
22
- // We use new-split to create a new pane in CMUX.
23
- // CMUX doesn't have a direct 'spawn' that returns a pane ID and runs a command
24
- // in one go while also returning the ID in a way we can easily capture for 'isAlive'.
25
- // However, 'new-split' returns the new surface ID.
26
-
27
- // Construct the command with environment variables
68
+ // Construct the full command with PI_ environment variables
28
69
  const envPrefix = Object.entries(options.env)
29
70
  .filter(([k]) => k.startsWith("PI_"))
30
71
  .map(([k, v]) => `${k}=${v}`)
31
72
  .join(" ");
32
-
73
+
33
74
  const fullCommand = envPrefix ? `env ${envPrefix} ${options.command}` : options.command;
34
75
 
35
- // CMUX new-split returns "OK <UUID>"
36
- const splitResult = execCommand("cmux", ["new-split", "right", "--command", fullCommand]);
37
-
76
+ // 1. Snapshot existing surfaces before the split
77
+ const before = this.listSurfaceRefs();
78
+
79
+ // 2. Create the split (without --command, which is not supported)
80
+ const splitResult = execCommand("cmux", ["new-split", "right"]);
81
+
38
82
  if (splitResult.status !== 0) {
39
83
  throw new Error(`cmux new-split failed with status ${splitResult.status}: ${splitResult.stderr}`);
40
84
  }
41
85
 
42
- const output = splitResult.stdout.trim();
43
- if (output.startsWith("OK ")) {
44
- const surfaceId = output.substring(3).trim();
45
- return surfaceId;
86
+ // 3. Poll for the newly created surface
87
+ const newSurface = this.waitForNewSurface(before);
88
+ if (!newSurface) {
89
+ throw new Error("cmux new-split succeeded but new surface was not found");
90
+ }
91
+
92
+ // 4. Use respawn-pane to run the command in the new surface
93
+ const respawnResult = execCommand("cmux", [
94
+ "respawn-pane",
95
+ "--surface", newSurface,
96
+ "--command", fullCommand,
97
+ ]);
98
+
99
+ if (respawnResult.status !== 0) {
100
+ throw new Error(`cmux respawn-pane failed with status ${respawnResult.status}: ${respawnResult.stderr}`);
46
101
  }
47
102
 
48
- throw new Error(`cmux new-split returned unexpected output: ${output}`);
103
+ return newSurface;
49
104
  }
50
105
 
51
106
  kill(paneId: string): void {
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { THINKING_LEVELS } from "./models";
3
+
4
+ describe("thinking levels", () => {
5
+ it("includes xhigh for teammate configuration", () => {
6
+ expect(THINKING_LEVELS).toContain("xhigh");
7
+ });
8
+ });
@@ -1,3 +1,7 @@
1
+ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
2
+
3
+ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
4
+
1
5
  export interface Member {
2
6
  agentId: string;
3
7
  name: string;
@@ -10,7 +14,7 @@ export interface Member {
10
14
  subscriptions: any[];
11
15
  prompt?: string;
12
16
  color?: string;
13
- thinking?: "off" | "minimal" | "low" | "medium" | "high";
17
+ thinking?: ThinkingLevel;
14
18
  planModeRequired?: boolean;
15
19
  backendType?: string;
16
20
  isActive?: boolean;
@@ -11,6 +11,7 @@ import {
11
11
  getAllPredefinedTeams,
12
12
  getAgentDefinition,
13
13
  getPredefinedTeam,
14
+ saveTeamTemplate,
14
15
  } from "./predefined-teams";
15
16
 
16
17
  describe("parseAgentFrontmatter", () => {
@@ -256,7 +257,8 @@ full:
256
257
 
257
258
  describe("getAllAgentDefinitions and getAllPredefinedTeams", () => {
258
259
  const globalDir = path.join(os.homedir(), ".pi", "agent", "agents");
259
- const globalTeamsDir = path.join(os.homedir(), ".pi", "agent");
260
+ const globalTeamsDir = path.join(os.homedir(), ".pi");
261
+ const legacyGlobalTeamsDir = path.join(os.homedir(), ".pi", "agent");
260
262
  const projectDir = path.join(os.tmpdir(), "pi-teams-test-project-" + Date.now());
261
263
  const projectAgentsDir = path.join(projectDir, ".pi", "agents");
262
264
  const projectTeamsDir = path.join(projectDir, ".pi");
@@ -264,6 +266,7 @@ describe("getAllAgentDefinitions and getAllPredefinedTeams", () => {
264
266
  // Store original files to restore later
265
267
  let originalGlobalAgents: string[] = [];
266
268
  let originalGlobalTeams: string | null = null;
269
+ let originalLegacyGlobalTeams: string | null = null;
267
270
 
268
271
  beforeEach(() => {
269
272
  // Create project directory
@@ -279,12 +282,31 @@ describe("getAllAgentDefinitions and getAllPredefinedTeams", () => {
279
282
  if (fs.existsSync(path.join(globalTeamsDir, "teams.yaml"))) {
280
283
  originalGlobalTeams = fs.readFileSync(path.join(globalTeamsDir, "teams.yaml"), "utf-8");
281
284
  }
285
+ if (fs.existsSync(path.join(legacyGlobalTeamsDir, "teams.yaml"))) {
286
+ originalLegacyGlobalTeams = fs.readFileSync(path.join(legacyGlobalTeamsDir, "teams.yaml"), "utf-8");
287
+ }
282
288
  });
283
289
 
284
290
  afterEach(() => {
285
291
  if (fs.existsSync(projectDir)) {
286
292
  fs.rmSync(projectDir, { recursive: true });
287
293
  }
294
+
295
+ const globalTeamsPath = path.join(globalTeamsDir, "teams.yaml");
296
+ if (originalGlobalTeams === null) {
297
+ if (fs.existsSync(globalTeamsPath)) fs.rmSync(globalTeamsPath);
298
+ } else {
299
+ fs.mkdirSync(globalTeamsDir, { recursive: true });
300
+ fs.writeFileSync(globalTeamsPath, originalGlobalTeams);
301
+ }
302
+
303
+ const legacyGlobalTeamsPath = path.join(legacyGlobalTeamsDir, "teams.yaml");
304
+ if (originalLegacyGlobalTeams === null) {
305
+ if (fs.existsSync(legacyGlobalTeamsPath)) fs.rmSync(legacyGlobalTeamsPath);
306
+ } else {
307
+ fs.mkdirSync(legacyGlobalTeamsDir, { recursive: true });
308
+ fs.writeFileSync(legacyGlobalTeamsPath, originalLegacyGlobalTeams);
309
+ }
288
310
  });
289
311
 
290
312
  it("combines global and project-local agents", () => {
@@ -330,6 +352,37 @@ custom:
330
352
  expect(result.find(t => t.name === "custom")).toBeDefined();
331
353
  expect(result.find(t => t.name === "custom")?.agents).toEqual(["agent1", "agent2"]);
332
354
  });
355
+
356
+ it("reads global predefined teams from ~/.pi/teams.yaml", () => {
357
+ fs.mkdirSync(globalTeamsDir, { recursive: true });
358
+ fs.writeFileSync(path.join(globalTeamsDir, "teams.yaml"), `
359
+ root-global:
360
+ - scout
361
+ `);
362
+
363
+ const result = getAllPredefinedTeams();
364
+
365
+ expect(result.find(t => t.name === "root-global")).toBeDefined();
366
+ expect(result.find(t => t.name === "root-global")?.agents).toEqual(["scout"]);
367
+ });
368
+
369
+ it("falls back to legacy ~/.pi/agent/teams.yaml when needed", () => {
370
+ const globalTeamsPath = path.join(globalTeamsDir, "teams.yaml");
371
+ if (fs.existsSync(globalTeamsPath)) {
372
+ fs.rmSync(globalTeamsPath);
373
+ }
374
+
375
+ fs.mkdirSync(legacyGlobalTeamsDir, { recursive: true });
376
+ fs.writeFileSync(path.join(legacyGlobalTeamsDir, "teams.yaml"), `
377
+ legacy-global:
378
+ - scout
379
+ `);
380
+
381
+ const result = getAllPredefinedTeams();
382
+
383
+ expect(result.find(t => t.name === "legacy-global")).toBeDefined();
384
+ expect(result.find(t => t.name === "legacy-global")?.agents).toEqual(["scout"]);
385
+ });
333
386
  });
334
387
 
335
388
  describe("getAgentDefinition and getPredefinedTeam", () => {
@@ -386,4 +439,73 @@ test-team:
386
439
  const result = getPredefinedTeam("non-existent", projectDir);
387
440
  expect(result).toBeUndefined();
388
441
  });
442
+ });
443
+
444
+ describe("saveTeamTemplate", () => {
445
+ const rootPiDir = path.join(os.homedir(), ".pi");
446
+ const globalAgentsDir = path.join(rootPiDir, "agent", "agents");
447
+ const globalTeamsPath = path.join(rootPiDir, "teams.yaml");
448
+ const projectDir = path.join(os.tmpdir(), "pi-teams-test-save-" + Date.now());
449
+
450
+ let originalGlobalTeams: string | null = null;
451
+ let originalAgentFiles = new Set<string>();
452
+
453
+ beforeEach(() => {
454
+ if (fs.existsSync(projectDir)) {
455
+ fs.rmSync(projectDir, { recursive: true });
456
+ }
457
+
458
+ if (fs.existsSync(globalTeamsPath)) {
459
+ originalGlobalTeams = fs.readFileSync(globalTeamsPath, "utf-8");
460
+ }
461
+ if (fs.existsSync(globalAgentsDir)) {
462
+ originalAgentFiles = new Set(fs.readdirSync(globalAgentsDir));
463
+ }
464
+ });
465
+
466
+ afterEach(() => {
467
+ if (fs.existsSync(projectDir)) {
468
+ fs.rmSync(projectDir, { recursive: true });
469
+ }
470
+
471
+ if (originalGlobalTeams === null) {
472
+ if (fs.existsSync(globalTeamsPath)) fs.rmSync(globalTeamsPath);
473
+ } else {
474
+ fs.mkdirSync(path.dirname(globalTeamsPath), { recursive: true });
475
+ fs.writeFileSync(globalTeamsPath, originalGlobalTeams);
476
+ }
477
+
478
+ if (fs.existsSync(globalAgentsDir)) {
479
+ for (const file of fs.readdirSync(globalAgentsDir)) {
480
+ if (!originalAgentFiles.has(file)) {
481
+ fs.rmSync(path.join(globalAgentsDir, file));
482
+ }
483
+ }
484
+ }
485
+ });
486
+
487
+ it("writes user-scoped teams to ~/.pi/teams.yaml and agents to ~/.pi/agent/agents", () => {
488
+ const result = saveTeamTemplate(
489
+ {
490
+ name: "audit-team",
491
+ members: [
492
+ {
493
+ name: "security-worker",
494
+ agentType: "teammate",
495
+ prompt: "Audit security issues",
496
+ },
497
+ ],
498
+ },
499
+ {
500
+ templateName: "audit-team",
501
+ scope: "user",
502
+ }
503
+ );
504
+
505
+ expect(result.teamsYamlPath).toBe(globalTeamsPath);
506
+ expect(result.agentsDir).toBe(globalAgentsDir);
507
+ expect(fs.existsSync(globalTeamsPath)).toBe(true);
508
+ expect(fs.readFileSync(globalTeamsPath, "utf-8")).toContain("audit-team:");
509
+ expect(fs.existsSync(path.join(globalAgentsDir, "security-worker.md"))).toBe(true);
510
+ });
389
511
  });
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ import { ThinkingLevel } from "./models";
4
5
 
5
6
  /**
6
7
  * Represents an agent definition from a .md file
@@ -10,7 +11,7 @@ export interface AgentDefinition {
10
11
  description: string;
11
12
  tools?: string[];
12
13
  model?: string;
13
- thinking?: "off" | "minimal" | "low" | "medium" | "high";
14
+ thinking?: ThinkingLevel;
14
15
  prompt: string;
15
16
  filePath: string;
16
17
  }
@@ -215,12 +216,18 @@ export function getAllPredefinedTeams(projectDir?: string): PredefinedTeam[] {
215
216
  const teams: PredefinedTeam[] = [];
216
217
  const seenNames = new Set<string>();
217
218
 
218
- // Global teams
219
- const globalDir = path.join(os.homedir(), ".pi", "agent");
220
- for (const team of discoverTeams(globalDir)) {
221
- if (!seenNames.has(team.name)) {
222
- seenNames.add(team.name);
223
- teams.push(team);
219
+ // Global teams: prefer the documented ~/.pi/teams.yaml location,
220
+ // but still fall back to the legacy ~/.pi/agent/teams.yaml path.
221
+ const globalDirs = [
222
+ path.join(os.homedir(), ".pi"),
223
+ path.join(os.homedir(), ".pi", "agent"),
224
+ ];
225
+ for (const globalDir of globalDirs) {
226
+ for (const team of discoverTeams(globalDir)) {
227
+ if (!seenNames.has(team.name)) {
228
+ seenNames.add(team.name);
229
+ teams.push(team);
230
+ }
224
231
  }
225
232
  }
226
233
 
@@ -293,7 +300,7 @@ export function generateAgentMarkdown(agent: {
293
300
  description?: string;
294
301
  tools?: string[];
295
302
  model?: string;
296
- thinking?: "off" | "minimal" | "low" | "medium" | "high";
303
+ thinking?: ThinkingLevel;
297
304
  prompt?: string;
298
305
  }): string {
299
306
  const lines: string[] = ["---"];
@@ -387,7 +394,7 @@ export function saveTeamTemplate(
387
394
  name: string;
388
395
  agentType: string;
389
396
  model?: string;
390
- thinking?: "off" | "minimal" | "low" | "medium" | "high";
397
+ thinking?: ThinkingLevel;
391
398
  prompt?: string;
392
399
  }>;
393
400
  defaultModel?: string;
@@ -395,12 +402,13 @@ export function saveTeamTemplate(
395
402
  options: SaveTeamTemplateOptions
396
403
  ): SaveTeamTemplateResult {
397
404
  // Determine output paths based on scope
398
- const baseDir = options.scope === "project"
399
- ? path.join(options.projectDir || process.cwd(), ".pi")
400
- : path.join(os.homedir(), ".pi", "agent");
405
+ const agentsDir = options.scope === "project"
406
+ ? path.join(options.projectDir || process.cwd(), ".pi", "agents")
407
+ : path.join(os.homedir(), ".pi", "agent", "agents");
401
408
 
402
- const agentsDir = path.join(baseDir, "agents");
403
- const teamsYamlPath = path.join(baseDir, "teams.yaml");
409
+ const teamsYamlPath = options.scope === "project"
410
+ ? path.join(options.projectDir || process.cwd(), ".pi", "teams.yaml")
411
+ : path.join(os.homedir(), ".pi", "teams.yaml");
404
412
 
405
413
  // Ensure agents directory exists
406
414
  if (!fs.existsSync(agentsDir)) {