pi-extended-teams 2.1.16

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 (130) hide show
  1. package/README.md +105 -0
  2. package/assets/pi-extended-teams-agent-navigation.png +0 -0
  3. package/assets/pi-extended-teams-in-action.png +0 -0
  4. package/extensions/agents/read-agent-report.ts +181 -0
  5. package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
  6. package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
  7. package/extensions/agents/read-agent.test.ts +3077 -0
  8. package/extensions/agents/read-agent.ts +1119 -0
  9. package/extensions/agents/write-agent.test.ts +513 -0
  10. package/extensions/agents/write-agent.ts +392 -0
  11. package/extensions/events/register-events.test.ts +465 -0
  12. package/extensions/events/register-events.ts +409 -0
  13. package/extensions/index.test.ts +1659 -0
  14. package/extensions/index.ts +1228 -0
  15. package/extensions/internal/agent-session-files.test.ts +164 -0
  16. package/extensions/internal/agent-session-files.ts +320 -0
  17. package/extensions/internal/debug.ts +44 -0
  18. package/extensions/internal/model-selection.ts +82 -0
  19. package/extensions/internal/pi-command.test.ts +191 -0
  20. package/extensions/internal/pi-command.ts +224 -0
  21. package/extensions/internal/pi-runtime-api.test.ts +43 -0
  22. package/extensions/internal/pi-runtime-api.ts +78 -0
  23. package/extensions/internal/schema.ts +19 -0
  24. package/extensions/internal/session-context-reference.test.ts +290 -0
  25. package/extensions/internal/session-context-reference.ts +430 -0
  26. package/extensions/internal/session-files.test.ts +225 -0
  27. package/extensions/internal/session-files.ts +273 -0
  28. package/extensions/internal/session-usage.ts +59 -0
  29. package/extensions/resources/spawn-resource-plan.test.ts +233 -0
  30. package/extensions/resources/spawn-resource-plan.ts +247 -0
  31. package/extensions/runtime/active-agent-sleep.test.ts +157 -0
  32. package/extensions/runtime/active-agent-sleep.ts +117 -0
  33. package/extensions/runtime/nested-read-agents.ts +22 -0
  34. package/extensions/runtime/pending-child-controller.test.ts +169 -0
  35. package/extensions/runtime/pending-child-controller.ts +280 -0
  36. package/extensions/runtime/types.ts +68 -0
  37. package/extensions/team/contracts.test.ts +111 -0
  38. package/extensions/team/lifecycle.test.ts +1095 -0
  39. package/extensions/team/lifecycle.ts +502 -0
  40. package/extensions/team/recipient-closure.test.ts +97 -0
  41. package/extensions/team/recipient-closure.ts +120 -0
  42. package/extensions/team/roster.test.ts +129 -0
  43. package/extensions/team/roster.ts +165 -0
  44. package/extensions/team/team-contracts.json +43 -0
  45. package/extensions/team/writer-screens.test.ts +50 -0
  46. package/extensions/team/writer-screens.ts +156 -0
  47. package/extensions/tools/agent-communication-tools.test.ts +306 -0
  48. package/extensions/tools/agent-communication-tools.ts +183 -0
  49. package/extensions/tools/coordination-tools.test.ts +670 -0
  50. package/extensions/tools/coordination-tools.ts +304 -0
  51. package/extensions/tools/delegation-guard.test.ts +95 -0
  52. package/extensions/tools/delegation-guard.ts +65 -0
  53. package/extensions/tools/file-claim-tools.test.ts +104 -0
  54. package/extensions/tools/file-claim-tools.ts +68 -0
  55. package/extensions/tools/model-tools.ts +53 -0
  56. package/extensions/tools/predefined-tools.test.ts +647 -0
  57. package/extensions/tools/predefined-tools.ts +331 -0
  58. package/extensions/tools/read-helper.test.ts +35 -0
  59. package/extensions/tools/task-runtime-tools.test.ts +334 -0
  60. package/extensions/tools/task-runtime-tools.ts +227 -0
  61. package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
  62. package/extensions/tools/team-tools.ts +1270 -0
  63. package/extensions/ui/agent-follow-view.test.ts +520 -0
  64. package/extensions/ui/agent-follow-view.ts +779 -0
  65. package/extensions/ui/agent-navigation.test.ts +52 -0
  66. package/extensions/ui/agent-navigation.ts +55 -0
  67. package/extensions/ui/ansi.ts +16 -0
  68. package/extensions/ui/extensions-command.test.ts +336 -0
  69. package/extensions/ui/extensions-command.ts +282 -0
  70. package/extensions/ui/favorite-models-command.test.ts +280 -0
  71. package/extensions/ui/favorite-models-command.ts +423 -0
  72. package/extensions/ui/frame.ts +77 -0
  73. package/extensions/ui/input.ts +17 -0
  74. package/extensions/ui/read-agent-status.test.ts +117 -0
  75. package/extensions/ui/read-agent-status.ts +125 -0
  76. package/extensions/ui/renderers.ts +243 -0
  77. package/extensions/ui/status-widget.test.ts +143 -0
  78. package/extensions/ui/status-widget.ts +440 -0
  79. package/extensions/ui-frame.test.ts +69 -0
  80. package/package.json +75 -0
  81. package/skills/teams.md +256 -0
  82. package/src/adapters/terminal-registry.ts +78 -0
  83. package/src/adapters/tmux-adapter.test.ts +276 -0
  84. package/src/adapters/tmux-adapter.ts +200 -0
  85. package/src/orchestration/index.ts +437 -0
  86. package/src/orchestration/orchestrator.test.ts +396 -0
  87. package/src/orchestration/types.ts +125 -0
  88. package/src/utils/atomic-json.ts +21 -0
  89. package/src/utils/claims.test.ts +137 -0
  90. package/src/utils/claims.ts +169 -0
  91. package/src/utils/hooks.test.ts +75 -0
  92. package/src/utils/hooks.ts +35 -0
  93. package/src/utils/lifecycle-tombstone.test.ts +105 -0
  94. package/src/utils/lifecycle-tombstone.ts +268 -0
  95. package/src/utils/lock.race.child.ts +44 -0
  96. package/src/utils/lock.race.test.ts +198 -0
  97. package/src/utils/lock.test.ts +90 -0
  98. package/src/utils/lock.ts +186 -0
  99. package/src/utils/messaging.test.ts +337 -0
  100. package/src/utils/messaging.ts +441 -0
  101. package/src/utils/model-resolution.test.ts +231 -0
  102. package/src/utils/model-resolution.ts +322 -0
  103. package/src/utils/models.test.ts +8 -0
  104. package/src/utils/models.ts +115 -0
  105. package/src/utils/paths.ts +81 -0
  106. package/src/utils/predefined-teams/types.ts +48 -0
  107. package/src/utils/predefined-teams.save-template.test.ts +74 -0
  108. package/src/utils/predefined-teams.test.ts +441 -0
  109. package/src/utils/predefined-teams.ts +471 -0
  110. package/src/utils/read-helper-queue.ts +99 -0
  111. package/src/utils/report-events.test.ts +154 -0
  112. package/src/utils/report-events.ts +222 -0
  113. package/src/utils/runtime.test.ts +314 -0
  114. package/src/utils/runtime.ts +261 -0
  115. package/src/utils/security.test.ts +43 -0
  116. package/src/utils/settings.test.ts +480 -0
  117. package/src/utils/settings.ts +645 -0
  118. package/src/utils/shared-memory.test.ts +80 -0
  119. package/src/utils/shared-memory.ts +81 -0
  120. package/src/utils/tasks.race.test.ts +44 -0
  121. package/src/utils/tasks.test.ts +229 -0
  122. package/src/utils/tasks.ts +396 -0
  123. package/src/utils/teams.ts +231 -0
  124. package/src/utils/terminal-adapter.ts +103 -0
  125. package/src/utils/thinking-levels.test.ts +56 -0
  126. package/src/utils/thinking-levels.ts +47 -0
  127. package/src/utils/workflow-metadata.test.ts +11 -0
  128. package/src/utils/workflow-metadata.ts +88 -0
  129. package/src/utils/write-queue.test.ts +251 -0
  130. package/src/utils/write-queue.ts +202 -0
@@ -0,0 +1,322 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { THINKING_LEVEL_NAMES } from "./thinking-levels";
5
+
6
+ export interface AvailableModel {
7
+ provider: string;
8
+ model: string;
9
+ }
10
+
11
+ export interface ModelResolutionConfig {
12
+ providerPriority?: string[];
13
+ }
14
+
15
+ export interface ResolvedModelResolutionConfig {
16
+ providerPriority: string[];
17
+ }
18
+
19
+ export interface PiModelSettings {
20
+ defaultProvider?: string;
21
+ defaultModel?: string;
22
+ enabledModels?: string[];
23
+ }
24
+
25
+ export interface SortedModel extends AvailableModel {
26
+ qualified: string;
27
+ preferred: boolean;
28
+ preferredIndex: number;
29
+ providerPriorityIndex: number;
30
+ }
31
+
32
+ const THINKING_LEVELS = new Set<string>(THINKING_LEVEL_NAMES);
33
+
34
+ function normalizeList(values: unknown): string[] | undefined {
35
+ if (!Array.isArray(values)) return undefined;
36
+
37
+ const normalized = values
38
+ .map((value) => String(value).trim())
39
+ .filter(Boolean);
40
+
41
+ return normalized.length > 0 ? Array.from(new Set(normalized)) : [];
42
+ }
43
+
44
+ function readJsonFile<T>(filePath: string): T | null {
45
+ if (!fs.existsSync(filePath)) return null;
46
+
47
+ try {
48
+ return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function readConfigFile(configPath: string): ModelResolutionConfig {
55
+ const parsed = readJsonFile<ModelResolutionConfig>(configPath);
56
+ if (!parsed) {
57
+ return {};
58
+ }
59
+
60
+ return {
61
+ providerPriority: normalizeList(parsed.providerPriority)?.map((value) => value.toLowerCase()),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Loads pi-extended-teams model-selection preferences.
67
+ *
68
+ * Supported locations:
69
+ * - ~/.pi/pi-extended-teams.json
70
+ * - <project>/.pi/pi-extended-teams.json
71
+ *
72
+ * Legacy ~/.pi/pi-teams.json and <project>/.pi/pi-teams.json files are still
73
+ * read for compatibility, but the pi-extended-teams paths win when present.
74
+ * Project-local config overrides global config.
75
+ */
76
+ export function loadModelResolutionConfig(options?: {
77
+ projectDir?: string;
78
+ homeDir?: string;
79
+ }): ResolvedModelResolutionConfig {
80
+ const homeDir = options?.homeDir ?? os.homedir();
81
+ const projectDir = options?.projectDir;
82
+
83
+ const legacyGlobalConfigPath = path.join(homeDir, ".pi", "pi-teams.json");
84
+ const globalConfigPath = path.join(homeDir, ".pi", "pi-extended-teams.json");
85
+ const legacyProjectConfigPath = projectDir ? path.join(projectDir, ".pi", "pi-teams.json") : null;
86
+ const projectConfigPath = projectDir ? path.join(projectDir, ".pi", "pi-extended-teams.json") : null;
87
+
88
+ const merged: ResolvedModelResolutionConfig = {
89
+ providerPriority: [],
90
+ };
91
+
92
+ for (const configPath of [legacyGlobalConfigPath, globalConfigPath, legacyProjectConfigPath, projectConfigPath]) {
93
+ if (!configPath) continue;
94
+
95
+ const config = readConfigFile(configPath);
96
+ if (config.providerPriority) {
97
+ merged.providerPriority = config.providerPriority;
98
+ }
99
+ }
100
+
101
+ return merged;
102
+ }
103
+
104
+ /**
105
+ * Loads pi settings relevant to model selection.
106
+ *
107
+ * Supported locations:
108
+ * - ~/.pi/agent/settings.json
109
+ * - <project>/.pi/settings.json
110
+ *
111
+ * Project-local settings override global settings.
112
+ */
113
+ export function loadPiModelSettings(options?: {
114
+ projectDir?: string;
115
+ homeDir?: string;
116
+ }): PiModelSettings {
117
+ const homeDir = options?.homeDir ?? os.homedir();
118
+ const projectDir = options?.projectDir;
119
+
120
+ const globalSettingsPath = path.join(homeDir, ".pi", "agent", "settings.json");
121
+ const projectSettingsPath = projectDir ? path.join(projectDir, ".pi", "settings.json") : null;
122
+
123
+ const merged: PiModelSettings = {};
124
+
125
+ for (const settingsPath of [globalSettingsPath, projectSettingsPath]) {
126
+ if (!settingsPath) continue;
127
+
128
+ const settings = readJsonFile<PiModelSettings>(settingsPath);
129
+ if (!settings) continue;
130
+
131
+ if (typeof settings.defaultProvider === "string" && settings.defaultProvider.trim()) {
132
+ merged.defaultProvider = settings.defaultProvider.trim();
133
+ }
134
+ if (typeof settings.defaultModel === "string" && settings.defaultModel.trim()) {
135
+ merged.defaultModel = settings.defaultModel.trim();
136
+ }
137
+ if (Array.isArray(settings.enabledModels)) {
138
+ merged.enabledModels = settings.enabledModels
139
+ .map((value) => String(value).trim())
140
+ .filter(Boolean);
141
+ }
142
+ }
143
+
144
+ return merged;
145
+ }
146
+
147
+ export function stripThinkingSuffix(specifier: string): string {
148
+ const trimmed = specifier.trim();
149
+ const lastColon = trimmed.lastIndexOf(":");
150
+ if (lastColon === -1) {
151
+ return trimmed;
152
+ }
153
+
154
+ const suffix = trimmed.slice(lastColon + 1).toLowerCase();
155
+ return THINKING_LEVELS.has(suffix) ? trimmed.slice(0, lastColon) : trimmed;
156
+ }
157
+
158
+ function escapeRegex(value: string): string {
159
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
160
+ }
161
+
162
+ function matchesPattern(pattern: string, value: string): boolean {
163
+ const normalizedPattern = pattern.toLowerCase();
164
+ const normalizedValue = value.toLowerCase();
165
+
166
+ if (normalizedPattern.includes("*")) {
167
+ const regex = new RegExp(`^${normalizedPattern.split("*").map(escapeRegex).join(".*")}$`, "i");
168
+ return regex.test(value);
169
+ }
170
+
171
+ return normalizedValue === normalizedPattern || normalizedValue.includes(normalizedPattern);
172
+ }
173
+
174
+ export function parseQualifiedModel(specifier: string): AvailableModel | null {
175
+ const cleaned = stripThinkingSuffix(specifier);
176
+ const slashIndex = cleaned.indexOf("/");
177
+ if (slashIndex === -1) {
178
+ return null;
179
+ }
180
+
181
+ const provider = cleaned.slice(0, slashIndex).trim();
182
+ const model = cleaned.slice(slashIndex + 1).trim();
183
+ if (!provider || !model) {
184
+ return null;
185
+ }
186
+
187
+ return { provider, model };
188
+ }
189
+
190
+ export function isQualifiedModel(specifier: string): boolean {
191
+ return parseQualifiedModel(specifier) !== null;
192
+ }
193
+
194
+ export function normalizeQualifiedModel(specifier: string): string | null {
195
+ const parsed = parseQualifiedModel(specifier);
196
+ return parsed ? `${parsed.provider}/${parsed.model}` : null;
197
+ }
198
+
199
+ function qualifyDefaultModel(settings: PiModelSettings): string | null {
200
+ if (!settings.defaultModel) {
201
+ return null;
202
+ }
203
+
204
+ if (isQualifiedModel(settings.defaultModel)) {
205
+ return normalizeQualifiedModel(settings.defaultModel);
206
+ }
207
+
208
+ if (settings.defaultProvider) {
209
+ return `${settings.defaultProvider}/${stripThinkingSuffix(settings.defaultModel)}`;
210
+ }
211
+
212
+ return null;
213
+ }
214
+
215
+ /**
216
+ * Builds a list of preferred fully-qualified models from pi settings.
217
+ *
218
+ * Order is significant:
219
+ * 1. preferredModels passed by the caller (must already be fully qualified)
220
+ * 2. pi default model (qualified via defaultProvider when necessary)
221
+ * 3. available models matching enabledModels patterns, in settings order
222
+ */
223
+ export function buildPreferredModelsFromSettings(
224
+ availableModels: AvailableModel[],
225
+ settings: PiModelSettings,
226
+ preferredModels: string[] = []
227
+ ): string[] {
228
+ const results: string[] = [];
229
+ const seen = new Set<string>();
230
+
231
+ const push = (specifier?: string | null) => {
232
+ if (!specifier) return;
233
+ const normalized = normalizeQualifiedModel(specifier);
234
+ if (!normalized || seen.has(normalized)) return;
235
+ seen.add(normalized);
236
+ results.push(normalized);
237
+ };
238
+
239
+ for (const model of preferredModels) {
240
+ push(model);
241
+ }
242
+
243
+ push(qualifyDefaultModel(settings));
244
+
245
+ for (const pattern of settings.enabledModels ?? []) {
246
+ const cleanedPattern = stripThinkingSuffix(pattern);
247
+ for (const availableModel of availableModels) {
248
+ const qualified = `${availableModel.provider}/${availableModel.model}`;
249
+ const target = cleanedPattern.includes("/") ? qualified : availableModel.model;
250
+ if (matchesPattern(cleanedPattern, target)) {
251
+ push(qualified);
252
+ }
253
+ }
254
+ }
255
+
256
+ return results;
257
+ }
258
+
259
+ export function listPreferredQualifiedModels(
260
+ availableModels: AvailableModel[],
261
+ options?: {
262
+ projectDir?: string;
263
+ homeDir?: string;
264
+ preferredModels?: string[];
265
+ }
266
+ ): string[] {
267
+ const settings = loadPiModelSettings(options);
268
+ return buildPreferredModelsFromSettings(availableModels, settings, options?.preferredModels ?? []);
269
+ }
270
+
271
+ export function sortAvailableModels(
272
+ availableModels: AvailableModel[],
273
+ options?: {
274
+ preferredModels?: string[];
275
+ providerPriority?: string[];
276
+ }
277
+ ): SortedModel[] {
278
+ const preferredModels = options?.preferredModels ?? [];
279
+ const providerPriority = (options?.providerPriority ?? []).map((value) => value.toLowerCase());
280
+
281
+ const preferredIndex = new Map(preferredModels.map((value, index) => [value, index]));
282
+ const providerPriorityIndex = new Map(providerPriority.map((value, index) => [value, index]));
283
+
284
+ return availableModels
285
+ .map((model) => {
286
+ const qualified = `${model.provider}/${model.model}`;
287
+ return {
288
+ ...model,
289
+ qualified,
290
+ preferred: preferredIndex.has(qualified),
291
+ preferredIndex: preferredIndex.get(qualified) ?? Number.MAX_SAFE_INTEGER,
292
+ providerPriorityIndex: providerPriorityIndex.get(model.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER,
293
+ };
294
+ })
295
+ .sort((a, b) => {
296
+ if (a.preferred !== b.preferred) {
297
+ return a.preferred ? -1 : 1;
298
+ }
299
+ if (a.preferredIndex !== b.preferredIndex) {
300
+ return a.preferredIndex - b.preferredIndex;
301
+ }
302
+ if (a.providerPriorityIndex !== b.providerPriorityIndex) {
303
+ return a.providerPriorityIndex - b.providerPriorityIndex;
304
+ }
305
+ const providerCompare = a.provider.localeCompare(b.provider);
306
+ if (providerCompare !== 0) {
307
+ return providerCompare;
308
+ }
309
+ return a.model.localeCompare(b.model);
310
+ });
311
+ }
312
+
313
+ export function isKnownQualifiedModel(model: string, availableModels: AvailableModel[]): boolean {
314
+ const normalized = normalizeQualifiedModel(model);
315
+ if (!normalized) {
316
+ return false;
317
+ }
318
+
319
+ return availableModels.some(
320
+ (availableModel) => `${availableModel.provider}/${availableModel.model}` === normalized
321
+ );
322
+ }
@@ -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
+ });
@@ -0,0 +1,115 @@
1
+ import { THINKING_LEVEL_NAMES, type ThinkingLevelName } from "./thinking-levels";
2
+
3
+ export const THINKING_LEVELS = THINKING_LEVEL_NAMES;
4
+ export type ThinkingLevel = ThinkingLevelName;
5
+
6
+ export interface Member {
7
+ agentId: string;
8
+ name: string;
9
+ agentType: string;
10
+ model?: string;
11
+ joinedAt: number;
12
+ tmuxPaneId: string;
13
+ windowId?: string;
14
+ cwd: string;
15
+ subscriptions: any[];
16
+ prompt?: string;
17
+ color?: string;
18
+ thinking?: ThinkingLevel;
19
+ planModeRequired?: boolean;
20
+ backendType?: string;
21
+ isActive?: boolean;
22
+ /** Optional programmatic orchestration/idempotency metadata. */
23
+ metadata?: Record<string, any>;
24
+ /** Authoritative identity for one admitted lifecycle run. Never stored in user metadata. */
25
+ lifecycleRunId?: string;
26
+ /** Runtime-owned delegation depth. Public lead spawns are depth 0; restricted children are depth 1. */
27
+ delegationDepth?: number;
28
+ /** Lead-approved opt-in for restricted nested read delegation. */
29
+ allowNestedReadAgents?: boolean;
30
+ /** Lead-approved lazy access to a filtered active-branch session snapshot. */
31
+ sessionContext?: "lazy";
32
+ /** Runtime-owned parent identity for a restricted nested read child. */
33
+ parentAgentName?: string;
34
+ /** Runtime-owned parent lifecycle identity for a restricted nested read child. */
35
+ parentLifecycleRunId?: string;
36
+ /** "read" agents run in-process (no pane); "write" agents spawn in tmux. */
37
+ role?: "read" | "write";
38
+ /** Optional category preset name from settings.json. */
39
+ category?: string;
40
+ /** Optional favorite model slot selected at spawn time. */
41
+ modelSlot?: string;
42
+ /** Agent that requested this read helper; final reports are delivered there. */
43
+ requestedBy?: string;
44
+ /** Marks extension-managed helper agents with special delivery/cleanup semantics. */
45
+ helperKind?: "read_helper";
46
+ }
47
+
48
+ export interface TeamConfig {
49
+ name: string;
50
+ description: string;
51
+ createdAt: number;
52
+ leadAgentId: string;
53
+ leadSessionId: string;
54
+ members: Member[];
55
+ defaultModel?: string;
56
+ separateWindows?: boolean;
57
+ /** Optional programmatic orchestration/idempotency metadata. */
58
+ metadata?: Record<string, any>;
59
+ }
60
+
61
+ export interface TaskFile {
62
+ id: string;
63
+ subject: string;
64
+ description: string;
65
+ activeForm?: string;
66
+ status: "pending" | "planning" | "in_progress" | "completed" | "deleted";
67
+ plan?: string;
68
+ planFeedback?: string;
69
+ blocks: string[];
70
+ blockedBy: string[];
71
+ owner?: string;
72
+ /** Monotonic task version for guarded programmatic updates. */
73
+ version?: number;
74
+ createdAt?: string;
75
+ updatedAt?: string;
76
+ metadata?: Record<string, any>;
77
+ }
78
+
79
+ export interface InboxMessage {
80
+ id?: string;
81
+ from: string;
82
+ text: string;
83
+ timestamp: string;
84
+ read: boolean;
85
+ summary?: string;
86
+ color?: string;
87
+ operationId?: string;
88
+ workflowRunId?: string;
89
+ metadata?: Record<string, any>;
90
+ }
91
+
92
+ export interface TeamReportEvent {
93
+ id: string;
94
+ teamName: string;
95
+ agentName: string;
96
+ role?: string;
97
+ status: "completed" | "failed";
98
+ report: string;
99
+ summary?: string;
100
+ reportPath?: string;
101
+ createdAt: number;
102
+ startedAt?: number;
103
+ elapsedMs?: number;
104
+ tokensUsed?: number;
105
+ costUsd?: number;
106
+ model?: string;
107
+ thinking?: string;
108
+ modelSlot?: string;
109
+ color?: string;
110
+ requestedBy?: string;
111
+ source: "read-agent" | "write-agent" | "lead-inbox" | "tool" | "workflow";
112
+ operationId?: string;
113
+ workflowRunId?: string;
114
+ metadata?: Record<string, any>;
115
+ }
@@ -0,0 +1,81 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+
5
+ export const PI_DIR = path.join(os.homedir(), ".pi");
6
+ export const TEAMS_DIR = path.join(PI_DIR, "teams");
7
+ export const TASKS_DIR = path.join(PI_DIR, "tasks");
8
+
9
+ export function ensureDirs() {
10
+ if (!fs.existsSync(PI_DIR)) fs.mkdirSync(PI_DIR);
11
+ if (!fs.existsSync(TEAMS_DIR)) fs.mkdirSync(TEAMS_DIR);
12
+ if (!fs.existsSync(TASKS_DIR)) fs.mkdirSync(TASKS_DIR);
13
+ }
14
+
15
+ export function sanitizeName(name: string): string {
16
+ // Allow only alphanumeric characters, hyphens, and underscores.
17
+ if (/[^a-zA-Z0-9_-]/.test(name)) {
18
+ throw new Error(`Invalid name: "${name}". Only alphanumeric characters, hyphens, and underscores are allowed.`);
19
+ }
20
+ return name;
21
+ }
22
+
23
+ export function teamDir(teamName: string) {
24
+ return path.join(TEAMS_DIR, sanitizeName(teamName));
25
+ }
26
+
27
+ export function taskDir(teamName: string) {
28
+ return path.join(TASKS_DIR, sanitizeName(teamName));
29
+ }
30
+
31
+ export function inboxPath(teamName: string, agentName: string) {
32
+ return path.join(teamDir(teamName), "inboxes", `${sanitizeName(agentName)}.json`);
33
+ }
34
+
35
+ export function runtimeStatusPath(teamName: string, agentName: string) {
36
+ return path.join(teamDir(teamName), "runtime", `${sanitizeName(agentName)}.json`);
37
+ }
38
+
39
+ export function sessionContextReferencePath(teamName: string, agentName: string, lifecycleRunId: string) {
40
+ return path.join(
41
+ teamDir(teamName),
42
+ "session-context",
43
+ `${sanitizeName(agentName)}--${sanitizeName(lifecycleRunId)}.md`,
44
+ );
45
+ }
46
+
47
+ export function lifecycleTombstonePath(teamName: string, agentName: string) {
48
+ return path.join(teamDir(teamName), "lifecycle", "quarantine", `${sanitizeName(agentName)}.json`);
49
+ }
50
+
51
+ export function configPath(teamName: string) {
52
+ return path.join(teamDir(teamName), "config.json");
53
+ }
54
+
55
+ export function claimsPath(teamName: string) {
56
+ return path.join(teamDir(teamName), "claims.json");
57
+ }
58
+
59
+ export function writeQueuePath(teamName: string) {
60
+ return path.join(teamDir(teamName), "write-queue.json");
61
+ }
62
+
63
+ export function readHelperQueuePath(teamName: string) {
64
+ return path.join(teamDir(teamName), "read-helper-queue.json");
65
+ }
66
+
67
+ export function sharedMemoryPath(teamName: string) {
68
+ return path.join(teamDir(teamName), "shared-memory.json");
69
+ }
70
+
71
+ export function reportEventsPath(teamName: string) {
72
+ return path.join(teamDir(teamName), "reports.json");
73
+ }
74
+
75
+ export function reportFilesDir() {
76
+ return path.join(PI_DIR, "agent", "reports");
77
+ }
78
+
79
+ export function leadSessionPath(teamName: string) {
80
+ return path.join(teamDir(teamName), "lead-session.json");
81
+ }
@@ -0,0 +1,48 @@
1
+ import type { ThinkingLevel } from "../models";
2
+
3
+ /**
4
+ * Represents an agent definition from a .md file
5
+ */
6
+ export interface AgentDefinition {
7
+ name: string;
8
+ description: string;
9
+ tools?: string[];
10
+ model?: string;
11
+ thinking?: ThinkingLevel;
12
+ prompt: string;
13
+ filePath: string;
14
+ }
15
+
16
+ /**
17
+ * Represents a predefined team from teams.yaml
18
+ */
19
+ export interface PredefinedTeam {
20
+ name: string;
21
+ agents: string[];
22
+ description?: string;
23
+ }
24
+
25
+ /**
26
+ * Options for saving a team as a template
27
+ */
28
+ export interface SaveTeamTemplateOptions {
29
+ templateName: string;
30
+ description?: string;
31
+ scope: "user" | "project";
32
+ projectDir?: string;
33
+ }
34
+
35
+ /**
36
+ * Result of saving a team as a template
37
+ */
38
+ export interface SaveTeamTemplateResult {
39
+ templateName: string;
40
+ agentsDir: string;
41
+ teamsYamlPath: string;
42
+ savedAgents: Array<{
43
+ name: string;
44
+ path: string;
45
+ existed: boolean;
46
+ }>;
47
+ templateExisted: boolean;
48
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { saveTeamTemplate } from "./predefined-teams";
6
+
7
+ describe("saveTeamTemplate", () => {
8
+ const rootPiDir = path.join(os.homedir(), ".pi");
9
+ const globalAgentsDir = path.join(rootPiDir, "agent", "agents");
10
+ const globalTeamsPath = path.join(rootPiDir, "teams.yaml");
11
+ const projectDir = path.join(os.tmpdir(), "pi-extended-teams-test-save-" + Date.now());
12
+
13
+ let originalGlobalTeams: string | null = null;
14
+ let originalAgentFiles = new Set<string>();
15
+
16
+ beforeEach(() => {
17
+ if (fs.existsSync(projectDir)) {
18
+ fs.rmSync(projectDir, { recursive: true });
19
+ }
20
+
21
+ if (fs.existsSync(globalTeamsPath)) {
22
+ originalGlobalTeams = fs.readFileSync(globalTeamsPath, "utf-8");
23
+ }
24
+ if (fs.existsSync(globalAgentsDir)) {
25
+ originalAgentFiles = new Set(fs.readdirSync(globalAgentsDir));
26
+ }
27
+ });
28
+
29
+ afterEach(() => {
30
+ if (fs.existsSync(projectDir)) {
31
+ fs.rmSync(projectDir, { recursive: true });
32
+ }
33
+
34
+ if (originalGlobalTeams === null) {
35
+ if (fs.existsSync(globalTeamsPath)) fs.rmSync(globalTeamsPath);
36
+ } else {
37
+ fs.mkdirSync(path.dirname(globalTeamsPath), { recursive: true });
38
+ fs.writeFileSync(globalTeamsPath, originalGlobalTeams);
39
+ }
40
+
41
+ if (fs.existsSync(globalAgentsDir)) {
42
+ for (const file of fs.readdirSync(globalAgentsDir)) {
43
+ if (!originalAgentFiles.has(file)) {
44
+ fs.rmSync(path.join(globalAgentsDir, file));
45
+ }
46
+ }
47
+ }
48
+ });
49
+
50
+ it("writes user-scoped teams to ~/.pi/teams.yaml and agents to ~/.pi/agent/agents", () => {
51
+ const result = saveTeamTemplate(
52
+ {
53
+ name: "audit-team",
54
+ members: [
55
+ {
56
+ name: "security-worker",
57
+ agentType: "teammate",
58
+ prompt: "Audit security issues",
59
+ },
60
+ ],
61
+ },
62
+ {
63
+ templateName: "audit-team",
64
+ scope: "user",
65
+ }
66
+ );
67
+
68
+ expect(result.teamsYamlPath).toBe(globalTeamsPath);
69
+ expect(result.agentsDir).toBe(globalAgentsDir);
70
+ expect(fs.existsSync(globalTeamsPath)).toBe(true);
71
+ expect(fs.readFileSync(globalTeamsPath, "utf-8")).toContain("audit-team:");
72
+ expect(fs.existsSync(path.join(globalAgentsDir, "security-worker.md"))).toBe(true);
73
+ });
74
+ });