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,409 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { Text } from "@mariozechner/pi-tui";
4
+ import * as paths from "../../src/utils/paths";
5
+ import * as runtime from "../../src/utils/runtime";
6
+ import * as messaging from "../../src/utils/messaging";
7
+ import * as teams from "../../src/utils/teams";
8
+ import { cleanupAgentSessionFolders, cleanupOrphanedTeams } from "../internal/session-files";
9
+ import { summarizeSessionUsage } from "../internal/session-usage";
10
+ import { formatElapsed, formatTokenCount } from "../ui/renderers";
11
+ import { isWorkflowSpawnedMember } from "../../src/utils/workflow-metadata";
12
+ import { FAVORITE_MODEL_SLOTS, loadSettings } from "../../src/utils/settings";
13
+ import { generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
14
+ import { cleanupStaleSessionContextReferences } from "../internal/session-context-reference";
15
+ import { cleanupStalePrivateAgentSessions } from "../internal/agent-session-files";
16
+
17
+ export const LEAD_ORCHESTRATION_GUIDANCE = `\n\npi-extended-teams lead orchestration rules:\n- Choose tiers by the agent's intended outcome, not by vague task importance. read-review is the normal default for focused review, verification, and bounded synthesis.\n- Use read-collect when the lane gathers bounded facts without owning the conclusion. Use read-analyze when it must explain behavior or root cause across connected evidence. Reserve read-critical for irreducible high-stakes security, architecture, concurrency, migration, or data-correctness reasoning.\n- For edits, use write-patch for a narrow localized change, write-feature for a bounded feature with a known design, write-system for a cross-cutting integration/refactor within explicitly claimed files, and write-critical only for high-risk security, concurrency, recovery, migration, or data-integrity changes.\n- Prefer the canonical read-*/write-* tiers. Legacy reading-*/writing-* names are compatibility aliases for this minor release, not intent guidance.\n- A spawned agent owns its assigned lane until it reports, blocks, fails, or the user cancels it. Do not duplicate, take over, test, edit, or synthesize that same lane in parallel; work only on clearly unrelated lanes.\n- When no unrelated work remains, wait literally idle for the automatic report prompt. Do not sleep, poll, repeatedly call read_inbox/check status, send nudges, do dummy work, or treat healthy silence as failure.\n- Wait for the actual report before synthesizing. Intervene only on a reported blocker/error, actual health failure, explicit user cancellation/change, or a genuinely finished agent that remains active.\n- For durable bug, security, or testing claims from an agent report or backlog, concrete, reproducible findings with file/line evidence or a focused failing regression may proceed directly to TDD repair.
18
+ - Use a separate read-only confirmation only when evidence is missing or weak, the claim is disputed, or irreducible high-risk uncertainty remains; never reconfirm an already confirmed finding.`;
19
+
20
+ export interface RegisterEventsOptions {
21
+ isTeammate: boolean;
22
+ agentName: string;
23
+ getTeamName(): string | null | undefined;
24
+ setSessionCtx(ctx: any): void;
25
+ terminal: any;
26
+ quietTrigger(content: string): void;
27
+ startLeadInboxPolling(): void;
28
+ startLeadWatchdog(): void;
29
+ buildRoster(teamName: string): Promise<any>;
30
+ formatRosterForPrompt(roster: any): string;
31
+ watchInboxDirectory?(
32
+ directory: string,
33
+ listener: (eventType: string, filename: string | Buffer | null) => void
34
+ ): fs.FSWatcher;
35
+ cleanupStaleSessionContextReferences?(): number;
36
+ cleanupStalePrivateAgentSessions?(): number;
37
+ }
38
+
39
+ export function isInboxFileWatchEvent(inboxFile: string, filename: string | Buffer | null | undefined): boolean {
40
+ const inboxBase = path.basename(inboxFile);
41
+ const changedName = filename?.toString();
42
+ return !changedName || changedName === inboxBase || changedName === `${inboxBase}.lock`;
43
+ }
44
+
45
+ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions): void {
46
+ let teammateWakeIfUnread: (() => Promise<void>) | null = null;
47
+ let teammatePendingInboxWake = false;
48
+ let teammateInboxWakeTimer: NodeJS.Timeout | null = null;
49
+ let teammateInboxPollTimer: NodeJS.Timeout | null = null;
50
+ let teammateInboxWatcher: fs.FSWatcher | null = null;
51
+ let teammateInboxDisposed = false;
52
+ let teammateLifecycleRunId: string | undefined = process.env.PI_LIFECYCLE_RUN_ID;
53
+ const teammateOneShotTimers = new Set<NodeJS.Timeout>();
54
+
55
+ const scheduleTeammateOneShot = (callback: () => void, delayMs: number) => {
56
+ const timer = setTimeout(() => {
57
+ teammateOneShotTimers.delete(timer);
58
+ if (!teammateInboxDisposed) callback();
59
+ }, delayMs);
60
+ teammateOneShotTimers.add(timer);
61
+ };
62
+
63
+ const disposeTeammateInbox = () => {
64
+ teammateInboxDisposed = true;
65
+ if (teammateInboxWakeTimer) clearTimeout(teammateInboxWakeTimer);
66
+ if (teammateInboxPollTimer) clearInterval(teammateInboxPollTimer);
67
+ for (const timer of teammateOneShotTimers) clearTimeout(timer);
68
+ teammateOneShotTimers.clear();
69
+ teammateInboxWatcher?.close();
70
+ teammateInboxWakeTimer = null;
71
+ teammateInboxPollTimer = null;
72
+ teammateInboxWatcher = null;
73
+ teammateWakeIfUnread = null;
74
+ teammatePendingInboxWake = false;
75
+ };
76
+
77
+ const scheduleTeammateInboxWake = (delayMs = 0) => {
78
+ if (teammateInboxDisposed || !teammateWakeIfUnread || teammateInboxWakeTimer) return;
79
+ teammateInboxWakeTimer = setTimeout(() => {
80
+ teammateInboxWakeTimer = null;
81
+ if (!teammateInboxDisposed) void teammateWakeIfUnread?.();
82
+ }, delayMs);
83
+ };
84
+
85
+ const modelContextWindow = (ctx: any): number => {
86
+ return typeof ctx?.model?.contextWindow === "number" ? ctx.model.contextWindow : 0;
87
+ };
88
+
89
+ const teammateRuntimeUpdates = (ctx: any, updates: Partial<runtime.AgentRuntimeStatus>, currentAssistantMessage?: any) => {
90
+ const usage = summarizeSessionUsage(ctx, currentAssistantMessage);
91
+ const contextWindow = modelContextWindow(ctx);
92
+ const hasMeasuredUsage = typeof usage.tokensUsed === "number" && usage.tokensUsed > 0;
93
+ let contextUsage: runtime.ContextUsageSnapshot = hasMeasuredUsage
94
+ ? { tokens: null, contextWindow, percent: null }
95
+ : runtime.initialContextUsage(contextWindow);
96
+ if (hasMeasuredUsage) {
97
+ try {
98
+ contextUsage = ctx?.getContextUsage?.() ?? contextUsage;
99
+ } catch {
100
+ // Context telemetry is best-effort while the teammate session is settling.
101
+ }
102
+ }
103
+ return {
104
+ ...updates,
105
+ ...(typeof usage.tokensUsed === "number" ? { tokensUsed: usage.tokensUsed } : {}),
106
+ contextUsage,
107
+ };
108
+ };
109
+
110
+ const writeTeammateRuntimeStatus = async (
111
+ teamName: string,
112
+ updates: Omit<Partial<runtime.AgentRuntimeStatus>, "teamName" | "agentName" | "lifecycleRunId">
113
+ ): Promise<runtime.AgentRuntimeStatus | null> => {
114
+ if (!teammateLifecycleRunId) throw new Error(`Missing lifecycle run id for ${options.agentName}.`);
115
+ try {
116
+ return await runtime.writeRuntimeStatus(teamName, options.agentName, teammateLifecycleRunId, updates);
117
+ } catch (error) {
118
+ // report_and_exit closes persistence before Pi emits its trailing
119
+ // tool/message/turn events. Those telemetry writes are benign no-ops.
120
+ if (runtime.isRuntimeStatusWriteRejectedError(error)) return null;
121
+ throw error;
122
+ }
123
+ };
124
+
125
+ pi.registerMessageRenderer?.("pi-extended-teams-report", (message: any, _renderOptions: any, theme: any) => {
126
+ const d = message.details || {};
127
+ const meta = [
128
+ d.elapsedMs ? formatElapsed(d.elapsedMs) : "",
129
+ typeof d.tokens === "number" ? `${formatTokenCount(d.tokens)} tok` : "",
130
+ ].filter(Boolean).join(" · ");
131
+ const mark = d.ok === false ? theme.fg("warning", "✗") : theme.fg("success", "✓");
132
+ const headline = `${mark} ${d.name || "agent"} reported${meta ? ` · ${meta}` : ""}`;
133
+ const body = typeof message.content === "string" ? message.content : "";
134
+ return new Text(`${theme.bold(headline)}\n\n${body}`, 0, 0);
135
+ });
136
+
137
+ pi.on("session_start", async (_event: any, ctx: any) => {
138
+ paths.ensureDirs();
139
+ // Local-only janitor pass: no model calls, no agent wakes. This prevents
140
+ // forced-shutdown leftovers (team dirs, debug.log, runtime files, pid files)
141
+ // from accumulating across Pi sessions.
142
+ try {
143
+ cleanupOrphanedTeams(options.terminal, { maxAgeMs: 24 * 60 * 60 * 1000 });
144
+ } catch {
145
+ // Session-start janitors are best-effort and must not block initialization.
146
+ }
147
+ try {
148
+ cleanupAgentSessionFolders(24 * 60 * 60 * 1000);
149
+ } catch {
150
+ // Session-start janitors are best-effort and must not block initialization.
151
+ }
152
+ options.setSessionCtx(ctx);
153
+ const teamName = options.getTeamName();
154
+
155
+ if (!options.isTeammate) {
156
+ try {
157
+ (options.cleanupStalePrivateAgentSessions ?? cleanupStalePrivateAgentSessions)();
158
+ } catch {
159
+ // Session-start janitors are best-effort and must not block initialization.
160
+ }
161
+ try {
162
+ (options.cleanupStaleSessionContextReferences ?? cleanupStaleSessionContextReferences)();
163
+ } catch {
164
+ // Session-start janitors are best-effort and must not block initialization.
165
+ }
166
+ const settings = loadSettings({ projectDir: ctx.cwd });
167
+ const configuredTiers = FAVORITE_MODEL_SLOTS.filter((slot) => {
168
+ const config = settings.favoriteModels[slot];
169
+ return !!config?.model && !!config.thinking;
170
+ });
171
+ if (configuredTiers.length === 0) {
172
+ ctx.ui?.notify?.(
173
+ "No agent intent tiers are configured. Define them with /agents-favorite-models before spawning agents. See README.md for intent-tier examples.",
174
+ "warning"
175
+ );
176
+ }
177
+ }
178
+
179
+ if (options.isTeammate) {
180
+ disposeTeammateInbox();
181
+ teammateInboxDisposed = false;
182
+ if (teamName) {
183
+ if (teams.teamExists(teamName)) {
184
+ const persistedRunId = await teams.ensureMemberLifecycleRunId(teamName, options.agentName, teammateLifecycleRunId);
185
+ if (teammateLifecycleRunId && persistedRunId !== teammateLifecycleRunId) {
186
+ throw new Error(`Refusing to start stale run ${teammateLifecycleRunId} for ${options.agentName}; current run is ${persistedRunId}.`);
187
+ }
188
+ teammateLifecycleRunId = persistedRunId;
189
+ } else {
190
+ // Compatibility for standalone/testing teammate contexts that have no
191
+ // persisted roster. Real spawned members are admitted before startup.
192
+ teammateLifecycleRunId ||= `compat-${generateLifecycleRunId()}`;
193
+ }
194
+ const pidFile = path.join(paths.teamDir(teamName), `${options.agentName}.pid`);
195
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
196
+ fs.writeFileSync(pidFile, process.pid.toString());
197
+ await runtime.writeRuntimeStatus(teamName, options.agentName, teammateLifecycleRunId, {
198
+ pid: process.pid,
199
+ startedAt: Date.now(),
200
+ lastHeartbeatAt: Date.now(),
201
+ ready: false,
202
+ currentAction: "starting",
203
+ activeToolName: undefined,
204
+ contextUsage: runtime.initialContextUsage(modelContextWindow(ctx)),
205
+ lastError: undefined,
206
+ });
207
+ }
208
+ ctx.ui.notify(`Teammate: ${options.agentName} (Team: ${teamName})`, "info");
209
+
210
+ if (options.terminal) {
211
+ const fullTitle = teamName ? `${teamName}: ${options.agentName}` : options.agentName;
212
+ const setIt = () => {
213
+ if ((ctx.ui as any).setTitle) (ctx.ui as any).setTitle(fullTitle);
214
+ options.terminal.setTitle(fullTitle);
215
+ };
216
+ setIt();
217
+ scheduleTeammateOneShot(setIt, 500);
218
+ scheduleTeammateOneShot(setIt, 2000);
219
+ scheduleTeammateOneShot(setIt, 5000);
220
+ }
221
+
222
+ scheduleTeammateOneShot(() => {
223
+ options.quietTrigger("read_inbox to get your instructions, then begin your work.");
224
+ }, 1000);
225
+
226
+ if (teamName) {
227
+ let wakeInFlight = false;
228
+ const wakeIfUnread = async () => {
229
+ if (teammateInboxDisposed) return;
230
+ if (wakeInFlight) {
231
+ teammatePendingInboxWake = true;
232
+ scheduleTeammateInboxWake(250);
233
+ return;
234
+ }
235
+ if (!ctx.isIdle()) {
236
+ teammatePendingInboxWake = true;
237
+ scheduleTeammateInboxWake(250);
238
+ return;
239
+ }
240
+ wakeInFlight = true;
241
+ teammatePendingInboxWake = false;
242
+ try {
243
+ const unread = await messaging.readInbox(teamName, options.agentName, true, false);
244
+ if (teammateInboxDisposed) return;
245
+ await writeTeammateRuntimeStatus(teamName, {
246
+ lastHeartbeatAt: Date.now(),
247
+ });
248
+ if (unread.length > 0) {
249
+ options.quietTrigger(`You have ${unread.length} new inbox message(s). Read them with read_inbox and act.`);
250
+ }
251
+ } catch (e) {
252
+ if (!teammateInboxDisposed) {
253
+ // Wake callers are fire-and-forget.
254
+ await writeTeammateRuntimeStatus(teamName, {
255
+ lastHeartbeatAt: Date.now(),
256
+ lastError: runtime.createRuntimeError(e),
257
+ }).catch(() => {});
258
+ }
259
+ } finally {
260
+ wakeInFlight = false;
261
+ if (teammatePendingInboxWake) {
262
+ scheduleTeammateInboxWake(ctx.isIdle() ? 0 : 250);
263
+ }
264
+ }
265
+ };
266
+ teammateWakeIfUnread = wakeIfUnread;
267
+
268
+ teammateInboxPollTimer = setInterval(wakeIfUnread, 30000);
269
+ try {
270
+ const inboxFile = paths.inboxPath(teamName, options.agentName);
271
+ fs.mkdirSync(path.dirname(inboxFile), { recursive: true });
272
+ const watchInboxDirectory = options.watchInboxDirectory ?? fs.watch;
273
+ teammateInboxWatcher = watchInboxDirectory(path.dirname(inboxFile), (_eventType, filename) => {
274
+ if (!teammateInboxDisposed && isInboxFileWatchEvent(inboxFile, filename)) {
275
+ scheduleTeammateInboxWake();
276
+ }
277
+ });
278
+ } catch {
279
+ // fs.watch is best-effort; the 30s poll remains a fallback.
280
+ }
281
+ }
282
+ } else if (teamName) {
283
+ options.startLeadInboxPolling();
284
+ options.startLeadWatchdog();
285
+ }
286
+ });
287
+
288
+ pi.on("session_shutdown", async () => {
289
+ disposeTeammateInbox();
290
+ if (!options.isTeammate) {
291
+ (options.cleanupStaleSessionContextReferences ?? cleanupStaleSessionContextReferences)();
292
+ }
293
+ });
294
+
295
+ pi.on("turn_start", async (_event: any, ctx: any) => {
296
+ const teamName = options.getTeamName();
297
+ if (options.isTeammate) {
298
+ const fullTitle = teamName ? `${teamName}: ${options.agentName}` : options.agentName;
299
+ if ((ctx.ui as any).setTitle) (ctx.ui as any).setTitle(fullTitle);
300
+ if (options.terminal) options.terminal.setTitle(fullTitle);
301
+ if (teamName) {
302
+ await writeTeammateRuntimeStatus(teamName, teammateRuntimeUpdates(ctx, {
303
+ lastHeartbeatAt: Date.now(),
304
+ currentAction: "thinking",
305
+ activeToolName: undefined,
306
+ }));
307
+ }
308
+ }
309
+ });
310
+
311
+ pi.on("tool_execution_start", async (event: any, ctx: any) => {
312
+ const teamName = options.getTeamName();
313
+ if (options.isTeammate && teamName) {
314
+ await writeTeammateRuntimeStatus(teamName, teammateRuntimeUpdates(ctx, {
315
+ lastHeartbeatAt: Date.now(),
316
+ currentAction: "working",
317
+ activeToolName: event?.toolName,
318
+ }));
319
+ }
320
+ });
321
+
322
+ pi.on("tool_execution_end", async (_event: any, ctx: any) => {
323
+ const teamName = options.getTeamName();
324
+ if (options.isTeammate && teamName) {
325
+ await writeTeammateRuntimeStatus(teamName, teammateRuntimeUpdates(ctx, {
326
+ lastHeartbeatAt: Date.now(),
327
+ currentAction: "thinking",
328
+ activeToolName: undefined,
329
+ }));
330
+ }
331
+ });
332
+
333
+ pi.on("message_end", async (event: any, ctx: any) => {
334
+ const teamName = options.getTeamName();
335
+ if (options.isTeammate && teamName && event.message?.role === "assistant") {
336
+ await writeTeammateRuntimeStatus(teamName, teammateRuntimeUpdates(ctx, {
337
+ lastHeartbeatAt: Date.now(),
338
+ }, event.message));
339
+ }
340
+ });
341
+
342
+ pi.on("turn_end", async (_event: any, ctx: any) => {
343
+ const teamName = options.getTeamName();
344
+ if (options.isTeammate && teamName) {
345
+ await writeTeammateRuntimeStatus(teamName, teammateRuntimeUpdates(ctx, {
346
+ lastHeartbeatAt: Date.now(),
347
+ currentAction: "thinking",
348
+ activeToolName: undefined,
349
+ }));
350
+ }
351
+ if (options.isTeammate && teammatePendingInboxWake && teammateWakeIfUnread) {
352
+ // Pi may emit turn_end before ctx.isIdle() flips to true. Defer the wake
353
+ // slightly so helper reports delivered during the writer's turn resume the
354
+ // writer immediately instead of waiting for the 30s fallback interval.
355
+ scheduleTeammateInboxWake(100);
356
+ }
357
+ });
358
+
359
+ let firstTurn = true;
360
+ pi.on("before_agent_start", async (event: any) => {
361
+ const teamName = options.getTeamName();
362
+ if (!options.isTeammate) {
363
+ return { systemPrompt: event.systemPrompt + LEAD_ORCHESTRATION_GUIDANCE };
364
+ }
365
+ if (firstTurn) {
366
+ firstTurn = false;
367
+
368
+ if (teamName) {
369
+ if (!teammateLifecycleRunId) throw new Error(`Missing lifecycle run id for ${options.agentName}.`);
370
+ await runtime.writeRuntimeStatus(teamName, options.agentName, teammateLifecycleRunId, {
371
+ lastHeartbeatAt: Date.now(),
372
+ ready: true,
373
+ currentAction: "thinking",
374
+ activeToolName: undefined,
375
+ });
376
+ }
377
+
378
+ let modelInfo = "";
379
+ let roleSpecificGuidance = "";
380
+ let rosterInfo = "";
381
+ if (teamName) {
382
+ try {
383
+ const teamConfig = await teams.readConfig(teamName);
384
+ const member = teamConfig.members.find(m => m.name === options.agentName);
385
+ if (member && member.model) {
386
+ modelInfo = `\nYou are currently using model: ${member.model}`;
387
+ if (member.thinking) modelInfo += ` with thinking level: ${member.thinking}`;
388
+ modelInfo += `. When reporting your model or thinking level, use these exact values.`;
389
+ }
390
+ if ((member?.role ?? "write") === "write") {
391
+ const workflowGuard = member && isWorkflowSpawnedMember(member)
392
+ ? "\n- Workflow mode: do not create helper fanout yourself. Ask team-lead with send_message for an explicit workflow assignment."
393
+ : "\n- If you need read-only help, ask team-lead with send_message. The lead decides whether to spawn another agent.";
394
+ roleSpecificGuidance = `\n\nEdit-agent rules:\n- Before editing or writing any repository file, call claim_file with every path you intend to change and wait for the claim to be granted.\n- If claim_file reports conflicts, do not edit those files; coordinate with your lead instead.${workflowGuard}\n- Release claims with release_file as soon as you are done editing those paths.\n- When your work is finished, call report_and_exit. It sends your final report, releases any remaining file claims, and shuts you down. Do not wait for the lead to kill you.`;
395
+ } else {
396
+ roleSpecificGuidance = `\n\nRead-agent rules:\n- You are read-only: investigate and report. Do not edit files or make any mutating changes.\n- When finished, produce your final report and stop. Do not wait for the lead to kill you.`;
397
+ }
398
+ rosterInfo = `\n\n${options.formatRosterForPrompt(await options.buildRoster(teamName))}\nUse this roster as a snapshot. If you need updated roster or liveness details, ask team-lead with send_message. Do not poll.`;
399
+ } catch {
400
+ // Ignore roster/model enrichment errors.
401
+ }
402
+ }
403
+
404
+ return {
405
+ systemPrompt: event.systemPrompt + `\n\nYou are spawned agent '${options.agentName}' in Pi session '${teamName}'.\nYour lead is 'team-lead'.${modelInfo}\n\nCore rules for every spawned agent:\n- NEVER sleep, busy-wait, or poll. Do not use bash sleep, while-true, or any wait/poll loop. The extension wakes you when messages arrive.\n- You cannot spawn, promote, or create other agents. If another agent is needed, use send_message to ask team-lead to decide and spawn.\n- Use send_message for direct communication and read_inbox when the extension wakes you or you expect a reply.\n- Progress reporting is required, not optional UI polish. After reading your initial instructions, call report_progress before your first work tool with a concise phrase describing what you are starting. Call it again whenever you change phase or evidence source, hit a blocker, or begin synthesis; never make more than 3 work-tool calls without a fresh progress update. Use a new phrase describing what you are doing now. It updates the activity widget without messaging or waking the lead; do not use it as a heartbeat.\n- When your work is done, report and exit cleanly. Do not wait for the lead to shut you down.${roleSpecificGuidance}${rosterInfo}\nStart by calling read_inbox to get your initial instructions.`,
406
+ };
407
+ }
408
+ });
409
+ }