pi-mono-all 1.0.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 (161) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENCE.md +7 -0
  3. package/node_modules/pi-common/package.json +22 -0
  4. package/node_modules/pi-common/src/auth-config.ts +290 -0
  5. package/node_modules/pi-common/src/auth.ts +63 -0
  6. package/node_modules/pi-common/src/cache.ts +60 -0
  7. package/node_modules/pi-common/src/errors.ts +47 -0
  8. package/node_modules/pi-common/src/http-client.ts +118 -0
  9. package/node_modules/pi-common/src/index.ts +7 -0
  10. package/node_modules/pi-common/src/rate-limiter.ts +32 -0
  11. package/node_modules/pi-common/src/tool-result.ts +27 -0
  12. package/node_modules/pi-mono-ask-user-question/CHANGELOG.md +185 -0
  13. package/node_modules/pi-mono-ask-user-question/README.md +226 -0
  14. package/node_modules/pi-mono-ask-user-question/index.ts +923 -0
  15. package/node_modules/pi-mono-ask-user-question/package.json +29 -0
  16. package/node_modules/pi-mono-auto-fix/CHANGELOG.md +59 -0
  17. package/node_modules/pi-mono-auto-fix/README.md +77 -0
  18. package/node_modules/pi-mono-auto-fix/index.ts +488 -0
  19. package/node_modules/pi-mono-auto-fix/package.json +23 -0
  20. package/node_modules/pi-mono-btw/CHANGELOG.md +180 -0
  21. package/node_modules/pi-mono-btw/README.md +24 -0
  22. package/node_modules/pi-mono-btw/index.ts +499 -0
  23. package/node_modules/pi-mono-btw/package.json +29 -0
  24. package/node_modules/pi-mono-clear/CHANGELOG.md +180 -0
  25. package/node_modules/pi-mono-clear/README.md +40 -0
  26. package/node_modules/pi-mono-clear/index.ts +45 -0
  27. package/node_modules/pi-mono-clear/package.json +29 -0
  28. package/node_modules/pi-mono-context/CHANGELOG.md +12 -0
  29. package/node_modules/pi-mono-context/README.md +74 -0
  30. package/node_modules/pi-mono-context/index.ts +641 -0
  31. package/node_modules/pi-mono-context/package.json +29 -0
  32. package/node_modules/pi-mono-context-guard/CHANGELOG.md +195 -0
  33. package/node_modules/pi-mono-context-guard/README.md +81 -0
  34. package/node_modules/pi-mono-context-guard/index.ts +212 -0
  35. package/node_modules/pi-mono-context-guard/package.json +23 -0
  36. package/node_modules/pi-mono-figma/CHANGELOG.md +59 -0
  37. package/node_modules/pi-mono-figma/README.md +236 -0
  38. package/node_modules/pi-mono-figma/__tests__/code-connect.test.ts +32 -0
  39. package/node_modules/pi-mono-figma/__tests__/figma-assets.test.ts +38 -0
  40. package/node_modules/pi-mono-figma/__tests__/figma-component-hints.test.ts +23 -0
  41. package/node_modules/pi-mono-figma/__tests__/figma-implementation-layout.test.ts +47 -0
  42. package/node_modules/pi-mono-figma/__tests__/figma-search.test.ts +51 -0
  43. package/node_modules/pi-mono-figma/__tests__/figma-summarizer.test.ts +65 -0
  44. package/node_modules/pi-mono-figma/__tests__/fixtures/complex-auto-layout.json +115 -0
  45. package/node_modules/pi-mono-figma/__tests__/fixtures/component-instance.json +50 -0
  46. package/node_modules/pi-mono-figma/__tests__/fixtures/hidden-and-vectors.json +28 -0
  47. package/node_modules/pi-mono-figma/__tests__/fixtures/variables-and-styles.json +40 -0
  48. package/node_modules/pi-mono-figma/docs/live-selection-bridge.md +16 -0
  49. package/node_modules/pi-mono-figma/index.ts +6 -0
  50. package/node_modules/pi-mono-figma/package.json +33 -0
  51. package/node_modules/pi-mono-figma/skills/figma/SKILL.md +143 -0
  52. package/node_modules/pi-mono-figma/src/code-connect.ts +110 -0
  53. package/node_modules/pi-mono-figma/src/figma-assets.ts +146 -0
  54. package/node_modules/pi-mono-figma/src/figma-cache.ts +6 -0
  55. package/node_modules/pi-mono-figma/src/figma-client.ts +471 -0
  56. package/node_modules/pi-mono-figma/src/figma-component-hints.ts +87 -0
  57. package/node_modules/pi-mono-figma/src/figma-implementation.ts +264 -0
  58. package/node_modules/pi-mono-figma/src/figma-schemas.ts +139 -0
  59. package/node_modules/pi-mono-figma/src/figma-search.ts +195 -0
  60. package/node_modules/pi-mono-figma/src/figma-summarizer.ts +673 -0
  61. package/node_modules/pi-mono-figma/src/figma-tokens.ts +57 -0
  62. package/node_modules/pi-mono-figma/src/figma-tools.ts +352 -0
  63. package/node_modules/pi-mono-linear/CHANGELOG.md +44 -0
  64. package/node_modules/pi-mono-linear/README.md +159 -0
  65. package/node_modules/pi-mono-linear/index.ts +6 -0
  66. package/node_modules/pi-mono-linear/package.json +30 -0
  67. package/node_modules/pi-mono-linear/skills/linear/SKILL.md +107 -0
  68. package/node_modules/pi-mono-linear/src/linear-client.ts +339 -0
  69. package/node_modules/pi-mono-linear/src/linear-queries.ts +101 -0
  70. package/node_modules/pi-mono-linear/src/linear-schemas.ts +90 -0
  71. package/node_modules/pi-mono-linear/src/linear-tools.ts +362 -0
  72. package/node_modules/pi-mono-loop/CHANGELOG.md +163 -0
  73. package/node_modules/pi-mono-loop/README.md +54 -0
  74. package/node_modules/pi-mono-loop/index.ts +291 -0
  75. package/node_modules/pi-mono-loop/package.json +26 -0
  76. package/node_modules/pi-mono-multi-edit/CHANGELOG.md +232 -0
  77. package/node_modules/pi-mono-multi-edit/README.md +244 -0
  78. package/node_modules/pi-mono-multi-edit/__tests__/classic.test.ts +277 -0
  79. package/node_modules/pi-mono-multi-edit/__tests__/diff.test.ts +77 -0
  80. package/node_modules/pi-mono-multi-edit/__tests__/patch.test.ts +287 -0
  81. package/node_modules/pi-mono-multi-edit/benchmark-edits.ts +966 -0
  82. package/node_modules/pi-mono-multi-edit/classic.ts +435 -0
  83. package/node_modules/pi-mono-multi-edit/diff.ts +143 -0
  84. package/node_modules/pi-mono-multi-edit/index.ts +266 -0
  85. package/node_modules/pi-mono-multi-edit/package.json +37 -0
  86. package/node_modules/pi-mono-multi-edit/patch.ts +463 -0
  87. package/node_modules/pi-mono-multi-edit/types.ts +53 -0
  88. package/node_modules/pi-mono-multi-edit/workspace.ts +85 -0
  89. package/node_modules/pi-mono-review/CHANGELOG.md +190 -0
  90. package/node_modules/pi-mono-review/README.md +30 -0
  91. package/node_modules/pi-mono-review/common.ts +930 -0
  92. package/node_modules/pi-mono-review/index.ts +8 -0
  93. package/node_modules/pi-mono-review/package.json +29 -0
  94. package/node_modules/pi-mono-review/review-tui.ts +194 -0
  95. package/node_modules/pi-mono-review/review.ts +119 -0
  96. package/node_modules/pi-mono-review/reviewer.ts +339 -0
  97. package/node_modules/pi-mono-sentinel/CHANGELOG.md +158 -0
  98. package/node_modules/pi-mono-sentinel/README.md +87 -0
  99. package/node_modules/pi-mono-sentinel/__tests__/output-scanner.test.ts +109 -0
  100. package/node_modules/pi-mono-sentinel/__tests__/permissions.test.ts +202 -0
  101. package/node_modules/pi-mono-sentinel/__tests__/whitelist.test.ts +59 -0
  102. package/node_modules/pi-mono-sentinel/guards/execution-tracker.ts +281 -0
  103. package/node_modules/pi-mono-sentinel/guards/output-scanner.ts +232 -0
  104. package/node_modules/pi-mono-sentinel/guards/permission-gate.ts +170 -0
  105. package/node_modules/pi-mono-sentinel/index.ts +43 -0
  106. package/node_modules/pi-mono-sentinel/package.json +26 -0
  107. package/node_modules/pi-mono-sentinel/patterns/permissions.ts +175 -0
  108. package/node_modules/pi-mono-sentinel/patterns/read-targets.ts +104 -0
  109. package/node_modules/pi-mono-sentinel/patterns/secrets.ts +143 -0
  110. package/node_modules/pi-mono-sentinel/session.ts +95 -0
  111. package/node_modules/pi-mono-sentinel/specs/2026/04/sentinel/001-permission-gate.md +145 -0
  112. package/node_modules/pi-mono-sentinel/types.ts +39 -0
  113. package/node_modules/pi-mono-sentinel/whitelist.ts +86 -0
  114. package/node_modules/pi-mono-simplify/CHANGELOG.md +163 -0
  115. package/node_modules/pi-mono-simplify/README.md +56 -0
  116. package/node_modules/pi-mono-simplify/index.ts +78 -0
  117. package/node_modules/pi-mono-simplify/package.json +29 -0
  118. package/node_modules/pi-mono-status-line/CHANGELOG.md +180 -0
  119. package/node_modules/pi-mono-status-line/README.md +96 -0
  120. package/node_modules/pi-mono-status-line/basic.ts +89 -0
  121. package/node_modules/pi-mono-status-line/expert.ts +689 -0
  122. package/node_modules/pi-mono-status-line/index.ts +54 -0
  123. package/node_modules/pi-mono-status-line/package.json +29 -0
  124. package/node_modules/pi-mono-team-mode/CHANGELOG.md +278 -0
  125. package/node_modules/pi-mono-team-mode/README.md +246 -0
  126. package/node_modules/pi-mono-team-mode/__tests__/agent-manager-transient.test.ts +75 -0
  127. package/node_modules/pi-mono-team-mode/__tests__/delegation-manager.test.ts +118 -0
  128. package/node_modules/pi-mono-team-mode/__tests__/formatters.test.ts +104 -0
  129. package/node_modules/pi-mono-team-mode/__tests__/model-config.test.ts +272 -0
  130. package/node_modules/pi-mono-team-mode/__tests__/notification-box.test.ts +34 -0
  131. package/node_modules/pi-mono-team-mode/__tests__/parallel-utils.test.ts +32 -0
  132. package/node_modules/pi-mono-team-mode/__tests__/pi-stream-parser.test.ts +64 -0
  133. package/node_modules/pi-mono-team-mode/__tests__/prompts.test.ts +106 -0
  134. package/node_modules/pi-mono-team-mode/__tests__/store.test.ts +164 -0
  135. package/node_modules/pi-mono-team-mode/__tests__/tasks.test.ts +267 -0
  136. package/node_modules/pi-mono-team-mode/__tests__/teammate-specs.test.ts +114 -0
  137. package/node_modules/pi-mono-team-mode/__tests__/widget.test.ts +41 -0
  138. package/node_modules/pi-mono-team-mode/__tests__/worktree.test.ts +78 -0
  139. package/node_modules/pi-mono-team-mode/core/chain-utils.ts +90 -0
  140. package/node_modules/pi-mono-team-mode/core/fs-utils.ts +44 -0
  141. package/node_modules/pi-mono-team-mode/core/model-config.ts +432 -0
  142. package/node_modules/pi-mono-team-mode/core/parallel-utils.ts +48 -0
  143. package/node_modules/pi-mono-team-mode/core/prompts.ts +158 -0
  144. package/node_modules/pi-mono-team-mode/core/store.ts +156 -0
  145. package/node_modules/pi-mono-team-mode/core/tasks.ts +99 -0
  146. package/node_modules/pi-mono-team-mode/core/teammate-specs.ts +124 -0
  147. package/node_modules/pi-mono-team-mode/core/types.ts +160 -0
  148. package/node_modules/pi-mono-team-mode/index.ts +825 -0
  149. package/node_modules/pi-mono-team-mode/managers/agent-manager.ts +654 -0
  150. package/node_modules/pi-mono-team-mode/managers/delegation-manager.ts +211 -0
  151. package/node_modules/pi-mono-team-mode/managers/task-manager.ts +238 -0
  152. package/node_modules/pi-mono-team-mode/managers/team-manager.ts +59 -0
  153. package/node_modules/pi-mono-team-mode/package.json +33 -0
  154. package/node_modules/pi-mono-team-mode/runtime/pi-stream-parser.ts +194 -0
  155. package/node_modules/pi-mono-team-mode/runtime/subprocess.ts +183 -0
  156. package/node_modules/pi-mono-team-mode/runtime/transient-session.ts +196 -0
  157. package/node_modules/pi-mono-team-mode/runtime/worktree.ts +90 -0
  158. package/node_modules/pi-mono-team-mode/ui/formatters.ts +149 -0
  159. package/node_modules/pi-mono-team-mode/ui/notification-box.ts +55 -0
  160. package/node_modules/pi-mono-team-mode/ui/widget.ts +94 -0
  161. package/package.json +76 -0
@@ -0,0 +1,654 @@
1
+ // Pi Team-Mode — Agent Manager
2
+
3
+ import type { TeamMateStore } from "../core/store.js";
4
+ import { generateTeammateId } from "../core/store.js";
5
+ import {
6
+ type ExecutionRuntime,
7
+ type IsolationMode,
8
+ type LiveTeammateMetrics,
9
+ type LiveTeammateSnapshot,
10
+ type SpawnOpts,
11
+ type ThinkingLevel,
12
+ type TeammateRecord,
13
+ type TeammateRunResult,
14
+ type TeammateStatus,
15
+ type TeammateSpec,
16
+ } from "../core/types.js";
17
+ import { runPi, type PiRun, type PiRunResult } from "../runtime/subprocess.js";
18
+ import { runTransientSession, type TransientSessionOpts } from "../runtime/transient-session.js";
19
+ import { cleanupWorktree, createWorktree, type WorktreeHandle } from "../runtime/worktree.js";
20
+ import { loadTeammateSpec } from "../core/teammate-specs.js";
21
+ import { TEAMMATE_SYSTEM_PROMPT_ADDENDUM } from "../core/prompts.js";
22
+ import type { PiStreamEvent } from "../runtime/pi-stream-parser.js";
23
+ import {
24
+ isModelTier,
25
+ loadModelConfig,
26
+ resolveModel,
27
+ splitThinkingSuffix,
28
+ type ModelTier,
29
+ type ResolvedModel,
30
+ } from "../core/model-config.js";
31
+
32
+ type LiveRun = {
33
+ run: PiRun;
34
+ record: TeammateRecord;
35
+ worktree?: WorktreeHandle;
36
+ description: string;
37
+ startedAt: number;
38
+ };
39
+
40
+ export type TeammateEndMetrics = {
41
+ toolUses?: number;
42
+ durationMs?: number;
43
+ metrics?: LiveTeammateMetrics;
44
+ transcriptPath?: string;
45
+ };
46
+
47
+ export type AgentManagerDeps = {
48
+ store: TeamMateStore;
49
+ getParentSessionId: () => string;
50
+ getDefaultCwd: () => string;
51
+ /**
52
+ * Invoked once a teammate transitions out of "running". Called for both
53
+ * foreground and background runs. The handler is expected to emit a
54
+ * `<task-notification>` to the parent session when appropriate.
55
+ */
56
+ onTeammateEnd?: (record: TeammateRecord, metrics: TeammateEndMetrics) => void;
57
+ /** Test seam for the in-process one-shot runner. */
58
+ runTransientSession?: (opts: TransientSessionOpts) => Promise<TeammateRunResult>;
59
+ };
60
+
61
+ export type ModelPick = {
62
+ provider?: string;
63
+ model?: string;
64
+ thinkingLevel?: ThinkingLevel;
65
+ rationale: string;
66
+ };
67
+
68
+ export class AgentManager {
69
+ private readonly liveRuns = new Map<string, LiveRun>();
70
+ private readonly metrics = new Map<string, LiveTeammateMetrics>();
71
+ private readonly descriptions = new Map<string, string>();
72
+ private readonly subscribers = new Set<() => void>();
73
+ private queuedCount = 0;
74
+ private notifyTimer: NodeJS.Timeout | undefined;
75
+
76
+ constructor(private readonly deps: AgentManagerDeps) {}
77
+
78
+ subscribeAll(cb: () => void): () => void {
79
+ this.subscribers.add(cb);
80
+ return () => {
81
+ this.subscribers.delete(cb);
82
+ };
83
+ }
84
+
85
+ setQueuedCount(count: number): void {
86
+ const next = Math.max(0, Math.floor(count));
87
+ if (next === this.queuedCount) return;
88
+ this.queuedCount = next;
89
+ this.scheduleNotify();
90
+ }
91
+
92
+ getQueuedCount(): number {
93
+ return this.queuedCount;
94
+ }
95
+
96
+ getLiveSnapshots(): LiveTeammateSnapshot[] {
97
+ return [...this.liveRuns.values()]
98
+ .map((live): LiveTeammateSnapshot | null => {
99
+ const metrics = this.metrics.get(live.record.id);
100
+ if (!metrics) return null;
101
+ return {
102
+ record: live.record,
103
+ metrics,
104
+ description: this.descriptions.get(live.record.id),
105
+ transcriptPath: this.deps.store.teammateSessionFile(live.record.id),
106
+ };
107
+ })
108
+ .filter((snap): snap is LiveTeammateSnapshot => snap !== null)
109
+ .sort((a, b) => a.metrics.startedAt - b.metrics.startedAt);
110
+ }
111
+
112
+ /**
113
+ * Spawn a new teammate. Returns immediately with a stub result when
114
+ * `background=true`; otherwise awaits the subprocess to exit and returns
115
+ * the final message.
116
+ */
117
+ async spawn(opts: SpawnOpts): Promise<TeammateRunResult> {
118
+ const runtime: ExecutionRuntime = opts.runtime ?? "subprocess";
119
+ if (runtime === "transient") return this.spawnTransient(opts);
120
+
121
+ const parentSessionId = this.deps.getParentSessionId();
122
+ const nameIndex = await this.deps.store.getNameIndex(parentSessionId);
123
+
124
+ const callerName = opts.name?.trim();
125
+ if (callerName && nameIndex[callerName]) {
126
+ throw new Error(
127
+ `teammate "${callerName}" already exists in this session — use send_message to continue it.`,
128
+ );
129
+ }
130
+
131
+ const teammateId = generateTeammateId(callerName);
132
+ const name = callerName || teammateId;
133
+
134
+ const team = opts.teamId ? await this.deps.store.loadTeam(opts.teamId) : null;
135
+ if (opts.teamId && !team) throw new Error(`unknown team: ${opts.teamId}`);
136
+
137
+ const isolation: IsolationMode = opts.isolation ?? team?.defaultIsolation ?? "none";
138
+
139
+ const baseCwd = opts.cwd ?? this.deps.getDefaultCwd();
140
+ let worktree: WorktreeHandle | undefined;
141
+ let cwd = baseCwd;
142
+ if (isolation === "worktree") {
143
+ worktree = await createWorktree(baseCwd, team?.worktreeBase);
144
+ cwd = worktree.path;
145
+ }
146
+
147
+ const spec = opts.subagentType
148
+ ? await loadTeammateSpec(baseCwd, opts.subagentType)
149
+ : null;
150
+
151
+ const pick = await this.resolveModel(opts.model ?? spec?.modelTier, opts.subagentType);
152
+ const thinkingLevel = opts.thinkingLevel ?? spec?.thinkingLevel ?? pick.thinkingLevel;
153
+
154
+ const now = new Date().toISOString();
155
+ const record: TeammateRecord = {
156
+ id: teammateId,
157
+ name,
158
+ teamId: opts.teamId,
159
+ subagentType: opts.subagentType,
160
+ model: pick.model,
161
+ provider: pick.provider,
162
+ thinkingLevel,
163
+ isolation,
164
+ cwd,
165
+ worktreeBranch: worktree?.branch,
166
+ status: "running",
167
+ background: opts.background ?? false,
168
+ createdAt: now,
169
+ updatedAt: now,
170
+ parentSessionId,
171
+ };
172
+ await this.deps.store.saveTeammate(record);
173
+
174
+ nameIndex[name] = teammateId;
175
+ await this.deps.store.setNameIndex(parentSessionId, nameIndex);
176
+
177
+ const initialMessage = buildInitialMessage(opts, spec?.description);
178
+ const run = runPi({
179
+ ...this.buildRunOptions(record, initialMessage, spec ?? undefined),
180
+ onEvent: (event) => this.applyEvent(record, event),
181
+ });
182
+
183
+ return this.track(
184
+ record,
185
+ run,
186
+ worktree,
187
+ opts.background ?? false,
188
+ pick.rationale,
189
+ opts.description,
190
+ );
191
+ }
192
+
193
+ private async spawnTransient(opts: SpawnOpts): Promise<TeammateRunResult> {
194
+ validateTransientOptions(opts);
195
+ const baseCwd = opts.cwd ?? this.deps.getDefaultCwd();
196
+ const spec = opts.subagentType
197
+ ? await loadTeammateSpec(baseCwd, opts.subagentType)
198
+ : null;
199
+ const pick = await this.resolveModel(opts.model ?? spec?.modelTier, opts.subagentType);
200
+ const thinkingLevel = opts.thinkingLevel ?? spec?.thinkingLevel ?? pick.thinkingLevel;
201
+ const teammateId = generateTeammateId();
202
+ const name = teammateId;
203
+ const initialMessage = buildInitialMessage(opts, spec?.description);
204
+ const runner = this.deps.runTransientSession ?? runTransientSession;
205
+ return runner({
206
+ id: teammateId,
207
+ name,
208
+ description: opts.description,
209
+ message: initialMessage,
210
+ cwd: baseCwd,
211
+ provider: pick.provider,
212
+ model: pick.model,
213
+ thinkingLevel,
214
+ modelRationale: pick.rationale,
215
+ spec: spec ?? undefined,
216
+ });
217
+ }
218
+
219
+ /** Resume an existing teammate by name. Context is preserved via pi's --session. */
220
+ async sendMessage(nameOrId: string, message: string): Promise<TeammateRunResult> {
221
+ const record = await this.resolveTeammate(nameOrId);
222
+ if (this.liveRuns.has(record.id)) {
223
+ throw new Error(
224
+ `teammate "${record.name}" is already running — wait for it to finish or use team_list to check status.`,
225
+ );
226
+ }
227
+
228
+ record.status = "running";
229
+ record.updatedAt = new Date().toISOString();
230
+ await this.deps.store.saveTeammate(record);
231
+
232
+ const spec = record.subagentType
233
+ ? await loadTeammateSpec(record.cwd, record.subagentType)
234
+ : null;
235
+
236
+ const run = runPi({
237
+ ...this.buildRunOptions(record, message, spec ?? undefined),
238
+ onEvent: (event) => this.applyEvent(record, event),
239
+ });
240
+
241
+ // Worktree cleanup only runs at the first spawn. Resume turns pass undefined.
242
+ return this.track(
243
+ record,
244
+ run,
245
+ undefined,
246
+ record.background,
247
+ "resume (reused initial model selection)",
248
+ `Continue ${record.name}`,
249
+ );
250
+ }
251
+
252
+ async stop(nameOrId: string): Promise<void> {
253
+ const record = await this.resolveTeammate(nameOrId);
254
+ const live = this.liveRuns.get(record.id);
255
+ if (live) live.run.abort();
256
+ const metrics = this.metrics.get(record.id);
257
+ if (metrics) {
258
+ metrics.exitReason = "stopped";
259
+ metrics.finishedAt = Date.now();
260
+ }
261
+ record.status = "stopped";
262
+ record.updatedAt = new Date().toISOString();
263
+ await this.deps.store.saveTeammate(record);
264
+ this.scheduleNotify();
265
+ }
266
+
267
+ /** Read the latest output of a teammate (used by the task_output tool). */
268
+ async output(nameOrId: string): Promise<TeammateRecord | null> {
269
+ try {
270
+ return await this.resolveTeammate(nameOrId);
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ /** List teammates owned by the current parent session. */
277
+ async list(): Promise<TeammateRecord[]> {
278
+ const parentSessionId = this.deps.getParentSessionId();
279
+ const all = await this.deps.store.listTeammates();
280
+ return all.filter((t) => t.parentSessionId === parentSessionId);
281
+ }
282
+
283
+ async get(nameOrId: string): Promise<TeammateRecord | null> {
284
+ try {
285
+ return await this.resolveTeammate(nameOrId);
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+
291
+ /** Abort all live runs and mark records as stopped. Called on session shutdown. */
292
+ async cleanup(): Promise<void> {
293
+ const entries = [...this.liveRuns.values()];
294
+ this.liveRuns.clear();
295
+ this.metrics.clear();
296
+ this.descriptions.clear();
297
+ this.queuedCount = 0;
298
+ const now = new Date().toISOString();
299
+ await Promise.all(
300
+ entries.map((live) => {
301
+ live.run.abort();
302
+ live.record.status = "stopped";
303
+ live.record.updatedAt = now;
304
+ return this.deps.store.saveTeammate(live.record).catch(() => {});
305
+ }),
306
+ );
307
+ this.scheduleNotify();
308
+ }
309
+
310
+ // --- internals ---
311
+
312
+ /**
313
+ * Pick `{ provider, model }` for a teammate. Priority:
314
+ * 1. Explicit fully-qualified caller override ("openai-codex/gpt-5.4").
315
+ * 2. Tier alias ("cheap"/"mid"/"deep"/"small"/"fast"/"big"/…) → resolveModel with override.
316
+ * 3. model-config.json role→tier mapping.
317
+ * 4. Nothing — let pi use its own defaults.
318
+ */
319
+ private async resolveModel(
320
+ override: string | undefined,
321
+ role: string | undefined,
322
+ ): Promise<ModelPick> {
323
+ const trimmed = override?.trim();
324
+ const config = await loadModelConfig();
325
+ const tierOverride = tierFromOverride(trimmed, config);
326
+ if (tierOverride) {
327
+ const resolved = resolveModel(config, role ?? "", tierOverride);
328
+ if (resolved) return packResolved(resolved);
329
+ }
330
+
331
+ // Explicit fully-qualified override bypasses model-config entirely.
332
+ if (trimmed) {
333
+ const slash = trimmed.indexOf("/");
334
+ if (slash >= 0) {
335
+ const split = splitThinkingSuffix(trimmed.slice(slash + 1));
336
+ return {
337
+ provider: trimmed.slice(0, slash),
338
+ model: split.model,
339
+ thinkingLevel: split.thinkingLevel,
340
+ rationale: "explicit spawn_agent.model override",
341
+ };
342
+ }
343
+ const split = splitThinkingSuffix(trimmed);
344
+ return {
345
+ model: split.model,
346
+ thinkingLevel: split.thinkingLevel,
347
+ rationale: "explicit spawn_agent.model override (bare id)",
348
+ };
349
+ }
350
+
351
+ const resolved = resolveModel(config, role ?? "");
352
+ if (resolved) return packResolved(resolved);
353
+
354
+ return { rationale: "no model-config catalog entry — letting pi use its own defaults" };
355
+ }
356
+
357
+ private async resolveTeammate(nameOrId: string): Promise<TeammateRecord> {
358
+ const parentSessionId = this.deps.getParentSessionId();
359
+ const nameIndex = await this.deps.store.getNameIndex(parentSessionId);
360
+ const id = nameIndex[nameOrId] ?? nameOrId;
361
+ const record = await this.deps.store.loadTeammate(id);
362
+ if (!record) throw new Error(`unknown teammate: ${nameOrId}`);
363
+ return record;
364
+ }
365
+
366
+ /** Build PiRunOptions from a teammate record + current message + optional spec. */
367
+ private buildRunOptions(
368
+ record: TeammateRecord,
369
+ message: string,
370
+ spec: TeammateSpec | undefined,
371
+ ) {
372
+ // Prepend the teammate communication addendum so every spawned
373
+ // subprocess knows it must use send_message to talk to peers.
374
+ const specBody = spec?.systemPrompt?.trim();
375
+ const systemPromptBody = specBody
376
+ ? `${TEAMMATE_SYSTEM_PROMPT_ADDENDUM}\n\n${specBody}`
377
+ : TEAMMATE_SYSTEM_PROMPT_ADDENDUM;
378
+ return {
379
+ message,
380
+ cwd: record.cwd,
381
+ sessionPath: this.deps.store.teammateSessionFile(record.id),
382
+ provider: record.provider,
383
+ model: record.model,
384
+ thinkingLevel: record.thinkingLevel,
385
+ tools: spec?.tools,
386
+ systemPromptBody,
387
+ parentSessionId: record.parentSessionId,
388
+ teammateName: record.name,
389
+ };
390
+ }
391
+
392
+ private async track(
393
+ record: TeammateRecord,
394
+ run: PiRun,
395
+ worktree: WorktreeHandle | undefined,
396
+ background: boolean,
397
+ modelRationale: string,
398
+ description: string,
399
+ ): Promise<TeammateRunResult> {
400
+ const startedAt = Date.now();
401
+ this.metrics.set(record.id, {
402
+ turns: 0,
403
+ toolUses: 0,
404
+ tokens: 0,
405
+ startedAt,
406
+ });
407
+ this.descriptions.set(record.id, description);
408
+ this.liveRuns.set(record.id, { run, record, worktree, description, startedAt });
409
+ this.scheduleNotify();
410
+
411
+ const finalize = async (): Promise<TeammateRunResult> => {
412
+ let runResult: PiRunResult | null = null;
413
+ let runError: Error | null = null;
414
+ try {
415
+ runResult = await run.promise;
416
+ } catch (err) {
417
+ runError = err as Error;
418
+ }
419
+ this.liveRuns.delete(record.id);
420
+
421
+ const metric = this.metrics.get(record.id);
422
+ if (metric) {
423
+ metric.finishedAt = Date.now();
424
+ }
425
+
426
+ const status: TeammateStatus = deriveStatus(runResult, runError, record.status);
427
+ const stderrTail = runResult?.stderr?.trim();
428
+ const finalMessage =
429
+ runResult?.finalMessage ||
430
+ (runError ? `[subprocess error] ${runError.message}` : "") ||
431
+ (status !== "completed" && stderrTail
432
+ ? `[pi exited ${runResult?.exitCode ?? "?"}] stderr:\n${stderrTail}`
433
+ : "");
434
+
435
+ let worktreeInfo: { path: string; branch: string } | undefined;
436
+ if (worktree) {
437
+ const cleanup = await cleanupWorktree(worktree).catch(() => null);
438
+ if (cleanup && !cleanup.removed) {
439
+ worktreeInfo = { path: cleanup.path, branch: cleanup.branch };
440
+ }
441
+ }
442
+
443
+ const updated: TeammateRecord = {
444
+ ...record,
445
+ status,
446
+ pid: undefined,
447
+ updatedAt: new Date().toISOString(),
448
+ lastResult: finalMessage,
449
+ lastExitCode: runResult?.exitCode ?? undefined,
450
+ };
451
+ await this.deps.store.saveTeammate(updated);
452
+
453
+ const baseMetric = this.metrics.get(record.id);
454
+ const finalMetrics = baseMetric
455
+ ? {
456
+ ...baseMetric,
457
+ exitReason: mapExitReason(status),
458
+ }
459
+ : undefined;
460
+ const transcriptPath = this.deps.store.teammateSessionFile(updated.id);
461
+ this.deps.onTeammateEnd?.(updated, {
462
+ toolUses: finalMetrics?.toolUses,
463
+ durationMs: Date.now() - startedAt,
464
+ metrics: finalMetrics,
465
+ transcriptPath,
466
+ });
467
+ this.metrics.delete(record.id);
468
+ this.descriptions.delete(record.id);
469
+ this.scheduleNotify();
470
+
471
+ return {
472
+ teammateId: updated.id,
473
+ name: updated.name,
474
+ description,
475
+ status,
476
+ result: finalMessage,
477
+ exitCode: runResult?.exitCode ?? null,
478
+ metrics: finalMetrics,
479
+ transcriptPath,
480
+ provider: record.provider,
481
+ model: record.model,
482
+ thinkingLevel: record.thinkingLevel,
483
+ modelRationale,
484
+ worktree: worktreeInfo,
485
+ durationMs: Date.now() - startedAt,
486
+ runtime: "subprocess",
487
+ };
488
+ };
489
+
490
+ if (!background) return finalize();
491
+
492
+ finalize().catch(() => {
493
+ /* errors are recorded inside finalize via saveTeammate */
494
+ });
495
+
496
+ return {
497
+ teammateId: record.id,
498
+ name: record.name,
499
+ description,
500
+ status: "running",
501
+ result:
502
+ `Agent spawned. task_id=${record.id}. ` +
503
+ `You will receive a <task-notification> when it finishes.`,
504
+ exitCode: null,
505
+ metrics: this.metrics.get(record.id),
506
+ transcriptPath: this.deps.store.teammateSessionFile(record.id),
507
+ provider: record.provider,
508
+ model: record.model,
509
+ thinkingLevel: record.thinkingLevel,
510
+ modelRationale,
511
+ background: true,
512
+ runtime: "subprocess",
513
+ };
514
+ }
515
+
516
+ private applyEvent(record: TeammateRecord, event: PiStreamEvent): void {
517
+ const metrics = this.metrics.get(record.id);
518
+ if (!metrics) return;
519
+
520
+ switch (event.type) {
521
+ case "assistant_delta": {
522
+ if (!metrics.activityHint) {
523
+ metrics.activityHint = "thinking…";
524
+ }
525
+ break;
526
+ }
527
+ case "assistant_message": {
528
+ metrics.turns += 1;
529
+ const usageTotal = event.usage?.totalTokens;
530
+ if (typeof usageTotal === "number" && Number.isFinite(usageTotal) && usageTotal > 0) {
531
+ metrics.tokens += usageTotal;
532
+ }
533
+ metrics.currentTool = undefined;
534
+ metrics.currentToolStartedAt = undefined;
535
+ metrics.activityHint = "responding…";
536
+ break;
537
+ }
538
+ case "tool_start": {
539
+ metrics.toolUses += 1;
540
+ metrics.currentTool = event.toolName;
541
+ metrics.currentToolStartedAt = Date.now();
542
+ metrics.activityHint = describeToolActivity(event.toolName, event.argsPreview);
543
+ break;
544
+ }
545
+ case "tool_end": {
546
+ metrics.activityHint = event.isError ? "tool error…" : "processing result…";
547
+ if (metrics.currentTool === event.toolName || !event.toolName) {
548
+ metrics.currentTool = undefined;
549
+ metrics.currentToolStartedAt = undefined;
550
+ }
551
+ break;
552
+ }
553
+ case "turn_end": {
554
+ metrics.activityHint = "waiting…";
555
+ break;
556
+ }
557
+ }
558
+
559
+ this.scheduleNotify();
560
+ }
561
+
562
+ private scheduleNotify(): void {
563
+ if (this.notifyTimer) return;
564
+ this.notifyTimer = setTimeout(() => {
565
+ this.notifyTimer = undefined;
566
+ for (const cb of this.subscribers) cb();
567
+ }, 80);
568
+ this.notifyTimer.unref();
569
+ }
570
+ }
571
+
572
+ // --- helpers ---
573
+
574
+ const TIER_ALIASES: Record<string, ModelTier> = {
575
+ small: "cheap",
576
+ fast: "cheap",
577
+ mini: "cheap",
578
+ default: "mid",
579
+ standard: "mid",
580
+ medium: "mid",
581
+ big: "deep",
582
+ large: "deep",
583
+ thinking: "deep",
584
+ high: "deep",
585
+ };
586
+
587
+ function tierFromOverride(value: string | undefined, config: Awaited<ReturnType<typeof loadModelConfig>>): ModelTier | undefined {
588
+ if (!value) return undefined;
589
+ const v = value.toLowerCase();
590
+ if (isModelTier(v)) return v;
591
+ if (config.tiers[v] || Object.values(config.roles).includes(v) || Object.values(config.roleTiers).includes(v)) return v;
592
+ return TIER_ALIASES[v];
593
+ }
594
+
595
+ function validateTransientOptions(opts: SpawnOpts): void {
596
+ if (opts.isolation === "worktree") {
597
+ throw new Error('runtime "transient" does not support isolation "worktree"; use runtime "subprocess" for worktree isolation.');
598
+ }
599
+ if (opts.background) {
600
+ throw new Error('runtime "transient" does not support run_in_background; use runtime "subprocess" for background workers.');
601
+ }
602
+ if (opts.teamId) {
603
+ throw new Error('runtime "transient" does not support team_name; transient runs are not durable team members.');
604
+ }
605
+ if (opts.name?.trim()) {
606
+ throw new Error('runtime "transient" does not support name; transient runs cannot be resumed with send_message.');
607
+ }
608
+ }
609
+
610
+ function packResolved(r: ResolvedModel): ModelPick {
611
+ return {
612
+ provider: r.provider,
613
+ model: r.model,
614
+ thinkingLevel: r.thinkingLevel,
615
+ rationale: `model-config: ${r.rationale}`,
616
+ };
617
+ }
618
+
619
+ function buildInitialMessage(opts: SpawnOpts, specDescription: string | undefined): string {
620
+ const header = `Task: ${opts.description}`;
621
+ const roleHint = specDescription ? `Role context: ${specDescription}` : "";
622
+ return [header, roleHint, "", opts.prompt].filter(Boolean).join("\n");
623
+ }
624
+
625
+ function deriveStatus(
626
+ result: PiRunResult | null,
627
+ error: Error | null,
628
+ previous: TeammateStatus,
629
+ ): TeammateStatus {
630
+ if (error || !result) return "failed";
631
+ if (result.exitSignal) return previous === "stopped" ? "stopped" : "failed";
632
+ return result.exitCode === 0 ? "completed" : "failed";
633
+ }
634
+
635
+ function mapExitReason(status: TeammateStatus): LiveTeammateMetrics["exitReason"] {
636
+ if (status === "completed") return "completed";
637
+ if (status === "stopped") return "stopped";
638
+ if (status === "running") return "wrapped_up";
639
+ if (status === "pending") return "aborted";
640
+ return "failed";
641
+ }
642
+
643
+ function describeToolActivity(toolName: string, argsPreview: string | undefined): string {
644
+ const tool = toolName.toLowerCase();
645
+ if (tool === "read") return "reading files…";
646
+ if (tool === "edit" || tool === "write") return "editing files…";
647
+ if (tool === "bash") return "running commands…";
648
+ if (tool === "grep" || tool === "glob") return "searching…";
649
+ if (argsPreview && argsPreview.length > 0) {
650
+ return `${toolName}: ${argsPreview.slice(0, 80)}${argsPreview.length > 80 ? "…" : ""}`;
651
+ }
652
+ return `${toolName}…`;
653
+ }
654
+