pi-extended-teams 2.1.17 → 2.2.3

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -12
  3. package/SECURITY.md +19 -0
  4. package/docs/access.md +35 -0
  5. package/extensions/agents/read-agent.ts +153 -4
  6. package/extensions/agents/write-agent.ts +8 -5
  7. package/extensions/events/register-events.ts +1 -1
  8. package/extensions/index.ts +49 -6
  9. package/extensions/runtime/nested-read-agents.ts +1 -1
  10. package/extensions/runtime/teammate-interrupt.ts +222 -0
  11. package/extensions/runtime/types.ts +15 -0
  12. package/extensions/team/team-contracts.json +1 -1
  13. package/extensions/tools/agent-status-tool.ts +270 -0
  14. package/extensions/tools/task-runtime-tools.ts +24 -3
  15. package/extensions/tools/team-tools.ts +76 -16
  16. package/extensions/ui/agent-follow-view.ts +128 -89
  17. package/extensions/ui/agent-navigation.ts +2 -0
  18. package/extensions/ui/frame.ts +33 -19
  19. package/extensions/ui/renderers.ts +4 -4
  20. package/extensions/ui/status-widget.ts +53 -20
  21. package/extensions/ui/theme.ts +34 -0
  22. package/package.json +15 -10
  23. package/skills/teams.md +14 -11
  24. package/src/adapters/tmux-adapter.ts +11 -0
  25. package/src/orchestration/index.ts +1 -1
  26. package/src/utils/runtime.ts +4 -0
  27. package/src/utils/settings.ts +23 -1
  28. package/src/utils/terminal-adapter.ts +7 -0
  29. package/assets/pi-extended-teams-agent-navigation.png +0 -0
  30. package/assets/pi-extended-teams-in-action.png +0 -0
  31. package/extensions/agents/read-agent-session-lifecycle.test.ts +0 -605
  32. package/extensions/agents/read-agent.test.ts +0 -3077
  33. package/extensions/agents/write-agent.test.ts +0 -513
  34. package/extensions/events/register-events.test.ts +0 -465
  35. package/extensions/index.test.ts +0 -1659
  36. package/extensions/internal/agent-session-files.test.ts +0 -164
  37. package/extensions/internal/pi-command.test.ts +0 -191
  38. package/extensions/internal/pi-runtime-api.test.ts +0 -43
  39. package/extensions/internal/session-context-reference.test.ts +0 -290
  40. package/extensions/internal/session-files.test.ts +0 -225
  41. package/extensions/resources/spawn-resource-plan.test.ts +0 -233
  42. package/extensions/runtime/active-agent-sleep.test.ts +0 -157
  43. package/extensions/runtime/pending-child-controller.test.ts +0 -169
  44. package/extensions/team/contracts.test.ts +0 -111
  45. package/extensions/team/lifecycle.test.ts +0 -1095
  46. package/extensions/team/recipient-closure.test.ts +0 -97
  47. package/extensions/team/roster.test.ts +0 -129
  48. package/extensions/team/writer-screens.test.ts +0 -50
  49. package/extensions/tools/agent-communication-tools.test.ts +0 -306
  50. package/extensions/tools/coordination-tools.test.ts +0 -670
  51. package/extensions/tools/delegation-guard.test.ts +0 -95
  52. package/extensions/tools/file-claim-tools.test.ts +0 -104
  53. package/extensions/tools/predefined-tools.test.ts +0 -647
  54. package/extensions/tools/read-helper.test.ts +0 -35
  55. package/extensions/tools/task-runtime-tools.test.ts +0 -334
  56. package/extensions/tools/team-tools.read-agent.test.ts +0 -1924
  57. package/extensions/ui/agent-follow-view.test.ts +0 -520
  58. package/extensions/ui/agent-navigation.test.ts +0 -52
  59. package/extensions/ui/extensions-command.test.ts +0 -336
  60. package/extensions/ui/favorite-models-command.test.ts +0 -280
  61. package/extensions/ui/read-agent-status.test.ts +0 -117
  62. package/extensions/ui/status-widget.test.ts +0 -143
  63. package/extensions/ui-frame.test.ts +0 -69
  64. package/src/adapters/tmux-adapter.test.ts +0 -276
  65. package/src/orchestration/orchestrator.test.ts +0 -396
  66. package/src/utils/claims.test.ts +0 -137
  67. package/src/utils/hooks.test.ts +0 -75
  68. package/src/utils/lifecycle-tombstone.test.ts +0 -105
  69. package/src/utils/lock.race.child.ts +0 -44
  70. package/src/utils/lock.race.test.ts +0 -198
  71. package/src/utils/lock.test.ts +0 -90
  72. package/src/utils/messaging.test.ts +0 -337
  73. package/src/utils/model-resolution.test.ts +0 -231
  74. package/src/utils/models.test.ts +0 -8
  75. package/src/utils/predefined-teams.save-template.test.ts +0 -74
  76. package/src/utils/predefined-teams.test.ts +0 -441
  77. package/src/utils/report-events.test.ts +0 -154
  78. package/src/utils/runtime.test.ts +0 -314
  79. package/src/utils/security.test.ts +0 -43
  80. package/src/utils/settings.test.ts +0 -480
  81. package/src/utils/shared-memory.test.ts +0 -80
  82. package/src/utils/tasks.race.test.ts +0 -44
  83. package/src/utils/tasks.test.ts +0 -229
  84. package/src/utils/thinking-levels.test.ts +0 -56
  85. package/src/utils/workflow-metadata.test.ts +0 -11
  86. package/src/utils/write-queue.test.ts +0 -251
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leonardo Pereira
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -6,7 +6,15 @@ pi-extended-teams lets the lead split work into bounded lanes, watch agents live
6
6
 
7
7
  Read agents are the default. Edit agents are opt-in and should own isolated files.
8
8
 
9
- [![Three agents working in parallel inside Pi](assets/pi-extended-teams-in-action.png)](assets/pi-extended-teams-in-action.png)
9
+ [![Three agents working in parallel inside Pi](https://raw.githubusercontent.com/dantetekanem/pi-extended-teams/main/assets/pi-extended-teams-in-action.png)](https://raw.githubusercontent.com/dantetekanem/pi-extended-teams/main/assets/pi-extended-teams-in-action.png)
10
+
11
+ ## Try without installing
12
+
13
+ ```bash
14
+ pi -e npm:pi-extended-teams
15
+ ```
16
+
17
+ This runs the published package for the current Pi invocation without adding it to your project configuration.
10
18
 
11
19
  ## Install and run
12
20
 
@@ -22,23 +30,22 @@ Or install directly from GitHub:
22
30
  pi install git:github.com/dantetekanem/pi-extended-teams
23
31
  ```
24
32
 
25
- Run `/agents-favorite-models` once in Pi and choose a model and thinking level for each intent tier.
26
-
27
33
  Then ask for help naturally:
28
34
 
29
35
  ```text
30
36
  Review the current changes with separate agents for correctness, test gaps, and security. Give me the evidence so I can make the final call.
31
37
  ```
32
38
 
33
- The current Pi session becomes the agent group automatically. There is no separate setup step.
39
+ The current Pi session becomes the agent group automatically. There is no separate setup step. Until you configure a tier, it uses the current lead-session model and thinking level. Run `/agents-favorite-models` when you want particular tiers to use different models.
34
40
 
35
41
  ## How it works
36
42
 
37
- - Read and edit agents run in-process and stay connected to the current Pi session.
43
+ - Read agents run in-process. Edit agents run in separate Pi sessions through the configured terminal adapter. Both stay connected to the lead session.
38
44
  - The activity card shows progress, intent tier, elapsed time, tokens, and tool activity.
39
- - You can open an agent's transcript, send it a message, or stop it.
45
+ - You can open an agent's transcript, send it a message, interrupt a stuck tool command, or stop it.
40
46
  - Completed reports return to the lead automatically and remain recoverable when needed.
41
- - Every spawn uses a configured intent tier instead of ad hoc model settings.
47
+ - `get_agent_status` gives the lead or an eligible nested parent one read-only snapshot of owned active, queued, stalled, or recently completed read and edit agents.
48
+ - Every spawn names an intent tier instead of choosing ad hoc model settings. Configured favorites take priority; unset tiers inherit the current lead model and thinking.
42
49
  - Edit agents can claim isolated files. Claims coordinate cooperative agents; they are not access control.
43
50
  - Lazy session context and nested read helpers are available when a bounded task needs them.
44
51
 
@@ -46,17 +53,19 @@ This works well for multi-angle code review, root-cause investigation, parallel
46
53
 
47
54
  ## Live control
48
55
 
49
- With the editor empty, press Down to open agent navigation. Use Down/Up to move, `l` to expand large tool logs, `m` to message an agent, `x` to stop it, and Escape to return.
56
+ With the editor empty, press Down to open agent navigation. Use Down/Up to move, `l` to expand large tool logs, `m` to message an agent, `i` to interrupt its currently running tool command, `x` to stop the whole agent, and Escape to return.
57
+
58
+ The lead can invoke the same command-only behavior with `interrupt_teammate({ agent_name: "agent" })`. It keeps the agent's session, task context, and file claims intact so you can send follow-up work. In-process cancellation is cooperative and may report that it is still pending; for tmux-backed agents, success means Pi's Escape key was delivered, not that command settlement was independently confirmed.
50
59
 
51
- [![Inspecting and messaging a running agent](assets/pi-extended-teams-agent-navigation.png)](assets/pi-extended-teams-agent-navigation.png)
60
+ [![Inspecting and messaging a running agent](https://raw.githubusercontent.com/dantetekanem/pi-extended-teams/main/assets/pi-extended-teams-agent-navigation.png)](https://raw.githubusercontent.com/dantetekanem/pi-extended-teams/main/assets/pi-extended-teams-agent-navigation.png)
52
61
 
53
- Completed reports wake the lead automatically. Do not poll with sleeps, loops, or repeated status checks. Use `check_teammate` only when one agent appears unhealthy or a persisted report needs recovery.
62
+ Completed reports wake the lead automatically. End the current turn to wait. One `get_agent_status` snapshot is allowed when current status is needed; do not poll with sleeps, loops, or repeated checks. Use `check_teammate` only when `get_agent_status` shows a suspected lifecycle failure or a persisted report needs recovery.
54
63
 
55
64
  The lead owns decomposition, integration, and acceptance. Pi packages and spawned agents run with your system permissions, so review project-local instructions and configuration through Pi's normal trust flow.
56
65
 
57
66
  ## Intent tiers
58
67
 
59
- Every spawn uses a configured `model_slot`:
68
+ Every spawn names a `model_slot`. Configured favorites take priority; otherwise the tier uses the current lead-session model and thinking level:
60
69
 
61
70
  | Tier | Use it for |
62
71
  | --- | --- |
@@ -89,10 +98,14 @@ For an edit, choose a write tier and name the files it may claim. Never run over
89
98
 
90
99
  ## Configuration
91
100
 
92
- Global settings live at `~/.pi/agent/pi-extended-teams/settings.json`. Project overrides live at `.pi/pi-extended-teams.json`. Favorite intent tiers are global so `/agents-favorite-models` and spawning use the same choices.
101
+ Global settings live at `~/.pi/agent/pi-extended-teams/settings.json`. Project overrides live at `.pi/pi-extended-teams.json`. Favorite intent tiers are global so `/agents-favorite-models` and spawning use the same choices. Configuring favorites is optional; an unset tier falls back to the current lead-session model and thinking level.
93
102
 
94
103
  Spawned sessions are private by default under `~/.pi/teams/<team>/agent-sessions/` and stay out of Pi's normal `/resume` picker.
95
104
 
105
+ ## Security and data access
106
+
107
+ Read [SECURITY.md](SECURITY.md) for private vulnerability reporting and [docs/access.md](docs/access.md) for the subprocess, filesystem, extension, hook, and network boundaries.
108
+
96
109
  ## Development
97
110
 
98
111
  ```bash
package/SECURITY.md ADDED
@@ -0,0 +1,19 @@
1
+ # Security policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are made on the latest published version of `pi-extended-teams`.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Do not open a public issue for a suspected vulnerability. Use [GitHub private vulnerability reporting](https://github.com/dantetekanem/pi-extended-teams/security/advisories/new) for this repository. Include a description, affected versions, reproduction steps, and impact.
10
+
11
+ If private reporting is unavailable, contact the maintainer through the repository owner's GitHub profile. Do not include credentials, private prompts, or other sensitive data in a public issue.
12
+
13
+ Reports are acknowledged after maintainer review. Disclosure timing and any fix will be coordinated with the reporter.
14
+
15
+ ## Trust boundary
16
+
17
+ This package coordinates local Pi sessions. A spawned teammate receives the prompt, project working directory, configured model selection, and the capabilities of its child Pi process. Review the access disclosure before using it with a project or credentials you do not intend a teammate to access.
18
+
19
+ See [docs/access.md](docs/access.md) for the process, file, extension, hook, and network boundaries.
package/docs/access.md ADDED
@@ -0,0 +1,35 @@
1
+ # Access and execution disclosure
2
+
3
+ `pi-extended-teams` is a local orchestration extension. It creates teammates that act in the working directory selected for the teammate. A teammate is not a sandbox or a security boundary.
4
+
5
+ ## Child processes
6
+
7
+ - Read teammates run in the lead Pi process through Pi's `AgentSession` API. They do not receive a tmux pane.
8
+ - Write teammates start a separate Pi process in a tmux pane. The extension builds and runs the local Pi launch command with the teammate's working directory, model and thinking configuration, selected extensions, and `PI_TEAM_NAME`, `PI_AGENT_NAME`, and `PI_LIFECYCLE_RUN_ID` environment variables.
9
+ - Before launching a write teammate, the extension runs a bounded local `pi models --all` preflight through `sh -c` to determine whether the configured model is available.
10
+ - On macOS, while at least one write teammate is active, the extension may run `/usr/bin/caffeinate -i -w <lead-pid>` to prevent idle system sleep. It terminates that helper when no write teammates remain or the extension is disposed.
11
+ - In a Herdr session, the extension can invoke the configured `herdr` binary to split, start, and close panes when moving an agent. The binary path defaults to `herdr` and can be set with `HERDR_BIN_PATH`.
12
+
13
+ ## Files and local state
14
+
15
+ The extension reads settings from `~/.pi/agent/pi-extended-teams/settings.json` and `<project>/.pi/pi-extended-teams.json`. Model-provider compatibility also reads the legacy `~/.pi/pi-extended-teams.json` path. Predefined agents can be read from `~/.pi/agent/agents/` and `<project>/.pi/agents/`; team templates can be read or written at `~/.pi/teams.yaml`, `~/.pi/agent/teams.yaml`, and `<project>/.pi/teams.yaml`.
16
+
17
+ For a team named `<team>`, coordination state is stored under `~/.pi/teams/<team>/`. This includes `config.json`, inboxes, runtime status, session-context references, lifecycle quarantine and tombstones, file claims, write and read-helper queues, shared memory, report events, lead-session metadata, debug logs, and private child transcripts under `agent-sessions/`. Task records are stored under `~/.pi/tasks/<team>/`, and agent reports may also be written under `~/.pi/agent/reports/`. Cleanup removes lifecycle and queue files when they are no longer needed.
18
+
19
+ The extension reads project extension sources only when Pi marks the same working directory trusted. Teammates receive the working directory supplied at spawn time. A write teammate can use any filesystem access granted to its child Pi process and enabled tools. A read teammate's tools are restricted by the extension, but it still receives prompt and project context supplied by the lead.
20
+
21
+ ## Extensions and hooks
22
+
23
+ A child Pi process starts with `--no-extensions`, then receives `pi-extended-teams` plus only lead-session extensions selected by the extension policy. The package does not automatically grant every available extension to a child. Project trust is forwarded only when the child uses the same working directory as the trusted lead session. Enabled extensions can add their own tools, hooks, file access, subprocesses, or network behavior; review them separately.
24
+
25
+ The package's documented project hook is `.pi/team-hooks/task_completed.sh`, which runs when a teammate marks a task complete. Hooks are project-supplied executable code and run with the permissions of the local Pi process. Do not enable or keep hooks you do not trust.
26
+
27
+ ## Network and model providers
28
+
29
+ The extension does not make application network requests itself. Pi and any enabled extensions may make network requests. In particular, a child Pi process can send its prompt and tool context to the configured model provider, subject to Pi's provider configuration. The model provider, enabled extensions, and tools determine any additional network access.
30
+
31
+ Use a model provider and extension set appropriate for the project data. Do not delegate secrets or data that you are not authorized to share with the configured provider or local tools.
32
+
33
+ ## Operator controls
34
+
35
+ The lead controls teammate creation, messages, task assignment, and stop/interrupt actions. A teammate cannot access lead-only process-control tools. File claims coordinate edits but do not enforce operating-system access control.
@@ -45,6 +45,45 @@ export { closeReadAgentMessageDelivery } from "./read-agent-session-lifecycle";
45
45
 
46
46
  const pendingParentWakeSignals = new WeakMap<RunningReadAgent, () => void>();
47
47
 
48
+ interface ReadAgentWakeState {
49
+ generation: number;
50
+ waiters: Set<() => void>;
51
+ }
52
+
53
+ const readAgentWakeStates = new WeakMap<RunningReadAgent, ReadAgentWakeState>();
54
+
55
+ function readAgentWakeState(agent: RunningReadAgent): ReadAgentWakeState {
56
+ let state = readAgentWakeStates.get(agent);
57
+ if (!state) {
58
+ state = { generation: 0, waiters: new Set() };
59
+ readAgentWakeStates.set(agent, state);
60
+ }
61
+ return state;
62
+ }
63
+
64
+ function readAgentWakeGeneration(agent: RunningReadAgent): number {
65
+ return readAgentWakeState(agent).generation;
66
+ }
67
+
68
+ function signalReadAgentWake(agent: RunningReadAgent): void {
69
+ const state = readAgentWakeState(agent);
70
+ state.generation += 1;
71
+ const waiters = Array.from(state.waiters);
72
+ state.waiters.clear();
73
+ for (const resolve of waiters) resolve();
74
+ }
75
+
76
+ function waitForReadAgentWake(agent: RunningReadAgent, generation: number): Promise<void> {
77
+ const state = readAgentWakeState(agent);
78
+ if (state.generation !== generation) return Promise.resolve();
79
+ return new Promise<void>((resolve) => { state.waiters.add(resolve); });
80
+ }
81
+
82
+ function disposeReadAgentWake(agent: RunningReadAgent): void {
83
+ signalReadAgentWake(agent);
84
+ readAgentWakeStates.delete(agent);
85
+ }
86
+
48
87
  export interface RunReadAgentOptions {
49
88
  isTeammate: boolean;
50
89
  getTeamName(): string | null | undefined;
@@ -95,6 +134,52 @@ function markReadAgentActivity(
95
134
  pushReadAgentEvent(agent, text);
96
135
  }
97
136
 
137
+ interface ReadAgentOperationOutcome {
138
+ generation: number;
139
+ interrupted: boolean;
140
+ }
141
+
142
+ async function runReadAgentSessionOperation(
143
+ state: RunningReadAgent,
144
+ operation: () => Promise<void>,
145
+ ): Promise<ReadAgentOperationOutcome> {
146
+ const generation = (state.operationGeneration ?? 0) + 1;
147
+ state.operationGeneration = generation;
148
+ state.activeOperationGeneration = generation;
149
+ state.completedOperationError = undefined;
150
+ let settleOperation!: () => void;
151
+ const operationSettlement = new Promise<void>((resolve) => { settleOperation = resolve; });
152
+ state.activeOperationSettlementPromise = operationSettlement;
153
+
154
+ try {
155
+ let failure: unknown;
156
+ try {
157
+ await operation();
158
+ } catch (error) {
159
+ failure = error;
160
+ }
161
+
162
+ if (state.activeOperationGeneration === generation) state.activeOperationGeneration = undefined;
163
+ const interrupted = state.interruptRequestedGeneration === generation;
164
+ if (interrupted) state.interruptRequestedGeneration = undefined;
165
+ state.completedOperationGeneration = generation;
166
+ state.completedOperationInterrupted = interrupted;
167
+ state.completedOperationError = interrupted ? undefined : failure;
168
+
169
+ if (interrupted) {
170
+ markReadAgentActivity(state, "command interrupted; waiting for lead", "thinking");
171
+ return { generation, interrupted: true };
172
+ }
173
+ if (failure) throw failure;
174
+ return { generation, interrupted: false };
175
+ } finally {
176
+ if (state.activeOperationSettlementPromise === operationSettlement) {
177
+ state.activeOperationSettlementPromise = undefined;
178
+ }
179
+ settleOperation();
180
+ }
181
+ }
182
+
98
183
  function refreshReadAgentStats(agent: RunningReadAgent, session: AgentSession, activityAlreadyMarked = false): void {
99
184
  const stats = session.getSessionStats();
100
185
  const tokensUsed = stats.tokens.total;
@@ -265,12 +350,20 @@ export async function sendMessageToRunningReadAgent(agent: RunningReadAgent | un
265
350
  }
266
351
 
267
352
  const session = agent.session;
268
- const delivery = session.isStreaming ? { deliverAs: "steer" as const } : undefined;
269
353
  const deliveryResult = enqueueReadAgentMessageDelivery(
270
354
  agent,
271
355
  agent.name,
272
- () => session.sendUserMessage(content, delivery)
356
+ async () => {
357
+ const pendingInterrupt = agent.operationInterruptPromise;
358
+ if (pendingInterrupt) await pendingInterrupt.catch(() => {});
359
+ if (session.isStreaming) {
360
+ await session.sendUserMessage(content, { deliverAs: "steer" as const });
361
+ return;
362
+ }
363
+ await runReadAgentSessionOperation(agent, () => session.sendUserMessage(content, undefined));
364
+ }
273
365
  );
366
+ signalReadAgentWake(agent);
274
367
  pendingParentWakeSignals.get(agent)?.();
275
368
  await deliveryResult;
276
369
  markReadAgentActivity(agent, "received lead message", "thinking");
@@ -350,6 +443,7 @@ async function recordReadAgentReportEvent(
350
443
  metadata: {
351
444
  ...(member.prompt ? { initialPrompt: member.prompt } : {}),
352
445
  ...(modelSlot ? { modelSlot } : {}),
446
+ ...(member.parentLifecycleRunId ? { parentLifecycleRunId: member.parentLifecycleRunId } : {}),
353
447
  ...reportMetadata,
354
448
  },
355
449
  });
@@ -769,6 +863,7 @@ export async function runReadAgentInProcess(
769
863
  ? [
770
864
  "This opted-in depth-0 write-feature/write-critical run may use restricted spawn_agent or spawn_swarm_agents for depth-1 read-only helpers. Choose any canonical read-* tier and any helper count, subject to the team's global read capacity and queue. Children report to you and cannot delegate.",
771
865
  "Use spawn_agent with only name (optional), prompt, and model_slot. Use spawn_swarm_agents with optional defaults.model_slot plus agents using only name (optional), prompt, and model_slot.",
866
+ "When waiting on children, end your turn without calling report_and_exit; each child report resumes you automatically. One get_agent_status snapshot is allowed when current status is needed, but never call it repeatedly.",
772
867
  "Do not request write tiers, cwd/team overrides, metadata, replacement of an active/queued name, or delegation outside your assigned scope.",
773
868
  ]
774
869
  : ["You cannot spawn or create other agents. If another agent is needed, use send_message to ask team-lead; only the lead decides and performs the spawn."]),
@@ -908,10 +1003,51 @@ export async function runReadAgentInProcess(
908
1003
  );
909
1004
  };
910
1005
 
1006
+ const waitForPostInterruptOperation = async (
1007
+ interruptedGeneration: number,
1008
+ initialWakeGeneration: number,
1009
+ ): Promise<boolean> => {
1010
+ state.operationAwaitingResume = true;
1011
+ let observedOperationGeneration = interruptedGeneration;
1012
+ let observedWakeGeneration = initialWakeGeneration;
1013
+ try {
1014
+ while (!state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
1015
+ const currentWakeGeneration = readAgentWakeGeneration(state);
1016
+ if (currentWakeGeneration === observedWakeGeneration) {
1017
+ const wake = waitForReadAgentWake(state, observedWakeGeneration);
1018
+ await (state.finished ? Promise.race([wake, state.finished]) : wake);
1019
+ }
1020
+ observedWakeGeneration = readAgentWakeGeneration(state);
1021
+ if (state.stopRequested || !options.isCurrentReadAgentRun(key, state)) return false;
1022
+
1023
+ const deliveryTail = state.messageDeliveryTail;
1024
+ if (deliveryTail) await deliveryTail.catch(() => {});
1025
+ const completedGeneration = state.completedOperationGeneration ?? 0;
1026
+ if (completedGeneration <= observedOperationGeneration) continue;
1027
+ observedOperationGeneration = completedGeneration;
1028
+ if (state.completedOperationError) throw state.completedOperationError;
1029
+ if (state.completedOperationInterrupted) continue;
1030
+ return true;
1031
+ }
1032
+ return false;
1033
+ } finally {
1034
+ state.operationAwaitingResume = false;
1035
+ }
1036
+ };
1037
+
911
1038
  state.acceptingMessages = true;
912
1039
  try {
913
- await session.prompt(prompt, { source: "extension" as any });
1040
+ const initialWakeGeneration = readAgentWakeGeneration(state);
1041
+ const initialOperation = await runReadAgentSessionOperation(
1042
+ state,
1043
+ () => session.prompt(prompt, { source: "extension" as any }),
1044
+ );
914
1045
  refreshReadAgentStats(state, session);
1046
+ if (initialOperation.interrupted) {
1047
+ const resumed = await waitForPostInterruptOperation(initialOperation.generation, initialWakeGeneration);
1048
+ if (!resumed) return;
1049
+ refreshReadAgentStats(state, session);
1050
+ }
915
1051
  if (pendingChildController && pendingChildParent) {
916
1052
  while (!submittedFinalReport && !state.stopRequested) {
917
1053
  const snapshot = pendingChildController.observeParent(pendingChildParent);
@@ -945,8 +1081,20 @@ export async function runReadAgentInProcess(
945
1081
  markReadAgentActivity(state, "recovering missing report", "thinking");
946
1082
  options.renderReadAgentStatus();
947
1083
  try {
948
- await session.prompt(EMPTY_REPORT_RECOVERY_PROMPT, { source: "extension" as any });
1084
+ const recoveryWakeGeneration = readAgentWakeGeneration(state);
1085
+ const recoveryOperation = await runReadAgentSessionOperation(
1086
+ state,
1087
+ () => session.prompt(EMPTY_REPORT_RECOVERY_PROMPT, { source: "extension" as any }),
1088
+ );
949
1089
  refreshReadAgentStats(state, session);
1090
+ if (recoveryOperation.interrupted) {
1091
+ const resumed = await waitForPostInterruptOperation(
1092
+ recoveryOperation.generation,
1093
+ recoveryWakeGeneration,
1094
+ );
1095
+ if (!resumed) return;
1096
+ refreshReadAgentStats(state, session);
1097
+ }
950
1098
  } catch (error) {
951
1099
  throw unavailableReportError(
952
1100
  `The report-only recovery turn failed: ${error instanceof Error ? error.message : String(error)}`,
@@ -1105,6 +1253,7 @@ export async function runReadAgentInProcess(
1105
1253
  }
1106
1254
  } finally {
1107
1255
  pendingParentWakeSignals.delete(state);
1256
+ disposeReadAgentWake(state);
1108
1257
  settleSessionCreation(state.session);
1109
1258
  // End run-owned activity even when persisted lifecycle cleanup is refused.
1110
1259
  if (state.heartbeatTimer) clearInterval(state.heartbeatTimer);
@@ -3,7 +3,7 @@ import * as teams from "../../src/utils/teams";
3
3
  import * as messaging from "../../src/utils/messaging";
4
4
  import * as runtime from "../../src/utils/runtime";
5
5
  import * as writeQueue from "../../src/utils/write-queue";
6
- import { loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
6
+ import { loadSettings, normalizeFavoriteModelSlot, requireConfiguredFavoriteModel, roleForFavoriteModelSlot } from "../../src/utils/settings";
7
7
  import type { Member } from "../../src/utils/models";
8
8
  import { isTeamsDebugEnabled, teamDebugLogPath, writeTeamsDebugEvent } from "../internal/debug";
9
9
  import { buildPiCommand, checkChildPiModelAvailability, getPiExtendedTeamsExtensionSource, getPiLaunchCommand } from "../internal/pi-command";
@@ -60,12 +60,15 @@ function stopAndVerifySpawnedPane(terminal: any, paneId: string): string | null
60
60
 
61
61
  function assertWriterUsesConfiguredLevel(member: Member): void {
62
62
  const settings = loadSettings({ projectDir: member.cwd });
63
- const level = requireFavoriteModelLevel(settings, member.modelSlot);
64
- if (level.role !== "write" || member.role !== "write") {
63
+ const configured = requireConfiguredFavoriteModel(settings, member.modelSlot);
64
+ const slot = configured?.slot ?? normalizeFavoriteModelSlot(member.modelSlot);
65
+ if (!slot) throw new Error(`Unknown favorite model slot "${String(member.modelSlot)}".`);
66
+ const role = roleForFavoriteModelSlot(slot);
67
+ if (role !== "write" || member.role !== "write") {
65
68
  throw new Error(`Write agent ${member.name} must use a write-* intent tier configured via /agents-favorite-models. Spawn agents by intent tier only.`);
66
69
  }
67
- if (member.model !== level.model || member.thinking !== level.thinking) {
68
- throw new Error(`Write agent ${member.name} must use configured intent tier ${level.slot}; direct model/thinking overrides are not allowed.`);
70
+ if (configured && (member.model !== configured.config.model || member.thinking !== configured.config.thinking)) {
71
+ throw new Error(`Write agent ${member.name} must use configured intent tier ${configured.slot}; direct model/thinking overrides are not allowed.`);
69
72
  }
70
73
  }
71
74
 
@@ -14,7 +14,7 @@ import { generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
14
14
  import { cleanupStaleSessionContextReferences } from "../internal/session-context-reference";
15
15
  import { cleanupStalePrivateAgentSessions } from "../internal/agent-session-files";
16
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.
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, end the turn. The extension resumes you when a report arrives. One get_agent_status snapshot is allowed when current status is needed; do not repeatedly call it, sleep, busy-wait, loop on inbox/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, or explicit user cancellation/change.\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
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
19
 
20
20
  export interface RegisterEventsOptions {
@@ -22,6 +22,7 @@ import { formatAnimatedProgress, formatContextUsage, formatElapsed, formatModelL
22
22
  import type { CompletedAgentReport, RunningReadAgent } from "./runtime/types.js";
23
23
  import { createPendingChildController } from "./runtime/pending-child-controller.js";
24
24
  import { createActiveAgentSleepController, runWithActiveAgentSleepAssertion } from "./runtime/active-agent-sleep.js";
25
+ import { createTeammateInterrupter } from "./runtime/teammate-interrupt.js";
25
26
  export { panelBgFill, framePanel, frameWidget, frameWidgetFullWidth, logWindowStart } from "./ui/frame.js";
26
27
  import * as messaging from "../src/utils/messaging";
27
28
  import * as teams from "../src/utils/teams";
@@ -62,6 +63,9 @@ export default function (pi: ExtensionAPI) {
62
63
  if (clearedTeamName === teamName) void drainReadHelperQueueOnce();
63
64
  });
64
65
  const runningReadAgents = new Map<string, RunningReadAgent>();
66
+ // Tmux writers have no nested AgentSession, but status rendering already
67
+ // maintains a lifecycle-fenced view of their active roster/runtime state.
68
+ const navigationWriterAgents = new Map<string, RunningReadAgent>();
65
69
  const completedAgentReports = new Map<string, CompletedAgentReport[]>();
66
70
  const TEAM_ACTIVITY_RENDER_DEBOUNCE_MS = 75;
67
71
  let readAgentStatusTimer: NodeJS.Timeout | null = null;
@@ -91,8 +95,8 @@ export default function (pi: ExtensionAPI) {
91
95
  getProjectTrusted: (cwd) => parentProjectTrustForSpawn(sessionCtx, cwd),
92
96
  createResourcePlan: createCurrentSpawnResourcePlan,
93
97
  sleepController: activeAgentSleepController,
94
- onWriterActive: (tab) => upsertWriterScreenTab(writerScreenState, tab),
95
- onWriterInactive: (targetTeamName, member) => removeWriterScreenTab(writerScreenState, { teamName: targetTeamName, name: member.name, paneId: member.tmuxPaneId }),
98
+ onWriterActive: (tab) => { upsertWriterScreenTab(writerScreenState, tab); markTeamActivityStatusDirty(0); },
99
+ onWriterInactive: (targetTeamName, member) => { removeWriterScreenTab(writerScreenState, { teamName: targetTeamName, name: member.name, paneId: member.tmuxPaneId }); markTeamActivityStatusDirty(0); },
96
100
  });
97
101
  const { shutdownTeammate, startLeadWatchdog, stopLeadWatchdog } = createLifecycleRuntime({
98
102
  isTeammate,
@@ -108,6 +112,7 @@ export default function (pi: ExtensionAPI) {
108
112
  onWriterInactive: (targetTeamName, member) => {
109
113
  releaseWriteAgentSleepAssertion(targetTeamName, member);
110
114
  removeWriterScreenTab(writerScreenState, { teamName: targetTeamName, name: member.name, paneId: member.tmuxPaneId });
115
+ markTeamActivityStatusDirty(0);
111
116
  },
112
117
  onTeammateClosing: (targetTeamName, member) => {
113
118
  if (!member.lifecycleRunId) return;
@@ -140,6 +145,13 @@ export default function (pi: ExtensionAPI) {
140
145
  return `${targetTeamName}:${targetAgentName}`;
141
146
  }
142
147
 
148
+ const interruptTeammate = createTeammateInterrupter({
149
+ terminal,
150
+ runningReadAgents,
151
+ readAgentKey,
152
+ getTeamName: () => teamName,
153
+ });
154
+
143
155
  function runInProcessAgentWithSleepAssertion(
144
156
  ...args: Parameters<typeof runReadAgentInProcess>
145
157
  ): ReturnType<typeof runReadAgentInProcess> {
@@ -232,6 +244,17 @@ export default function (pi: ExtensionAPI) {
232
244
  && runtimeHeartbeatIsRecent(runtimeStatus, now);
233
245
  }
234
246
 
247
+ function navigationWriterAgent(teamName: string, member: Member, status: runtime.AgentRuntimeStatus): RunningReadAgent {
248
+ return {
249
+ runId: member.lifecycleRunId!, name: member.name, teamName,
250
+ startedAt: status.startedAt || member.joinedAt, tokensUsed: 0, contextUsage: status.contextUsage,
251
+ status: status.currentAction === "thinking" ? "thinking" : status.currentAction === "starting" ? "starting" : "working",
252
+ recentEvents: [], lastActivityAt: status.lastHeartbeatAt || member.joinedAt, role: "write",
253
+ model: member.model, thinking: member.thinking, modelSlot: canonicalPersistedModelSlot(member.modelSlot),
254
+ latestProgress: status.latestProgress,
255
+ };
256
+ }
257
+
235
258
  function runtimeOnlyStatusLabel(status: runtime.AgentRuntimeStatus, now: number): string {
236
259
  if (!runtimeHeartbeatIsRecent(status, now)) return "stale";
237
260
  if (status.currentAction && status.currentAction !== "done") return status.currentAction;
@@ -496,12 +519,13 @@ export default function (pi: ExtensionAPI) {
496
519
  teamActivityWidgetMounted = true;
497
520
  sessionCtx.ui.setWidget(
498
521
  "01-pi-extended-teams-readers",
499
- (tui: any) => {
522
+ (tui: any, theme: any) => {
500
523
  teamActivityWidgetTui = tui;
501
524
  return teamActivityStatusWidget(
502
525
  () => teamActivityStatusSnapshot,
503
526
  isTeamActivityExpanded,
504
- () => tui.requestRender?.()
527
+ () => tui.requestRender?.(),
528
+ theme
505
529
  );
506
530
  },
507
531
  { placement: "belowEditor" }
@@ -591,6 +615,12 @@ export default function (pi: ExtensionAPI) {
591
615
  .filter(member => member.name !== "team-lead" && member.isActive !== false && memberActivityRole(member) === "write")
592
616
  .filter(member => !!(member.tmuxPaneId && terminal?.isAlive?.(member.tmuxPaneId)) && !runningReadAgents.has(readAgentKey(activityTeamName, member.name)) && !runtimeOnlyMemberNames.has(member.name)) ?? []
593
617
  : [];
618
+ navigationWriterAgents.clear();
619
+ for (const { member, runtimeStatus } of runtimeOnlyWriteMembers) {
620
+ if (member.tmuxPaneId && member.lifecycleRunId === runtimeStatus.lifecycleRunId) {
621
+ navigationWriterAgents.set(member.name, navigationWriterAgent(activityTeamName!, member, runtimeStatus));
622
+ }
623
+ }
594
624
  const unreadLeadMessages = activityTeamName ? await messaging.readInbox(activityTeamName, agentName, true, false).catch(() => []) : [];
595
625
  leadInboxUnreadCount = unreadLeadMessages.length;
596
626
  clearLeadInboxWidgetOnce();
@@ -1083,8 +1113,20 @@ export default function (pi: ExtensionAPI) {
1083
1113
  if (!isTeammate) {
1084
1114
  pi.on("session_start", async (_event: any, ctx: any) => {
1085
1115
  installAgentNavigation(ctx, {
1086
- getAgents: () => Array.from(runningReadAgents.values())
1087
- .filter(agent => !teamName || agent.teamName === teamName),
1116
+ getAgents: () => {
1117
+ const readers = Array.from(runningReadAgents.values())
1118
+ .filter(agent => !teamName || agent.teamName === teamName);
1119
+ return [...readers, ...Array.from(navigationWriterAgents.values())
1120
+ .filter(agent => !teamName || agent.teamName === teamName)]
1121
+ .filter((agent, index, agents) => agents.findIndex(candidate => candidate.name === agent.name) === index);
1122
+ },
1123
+ interruptAgent: async (name: string) => {
1124
+ const result = await interruptTeammate(name);
1125
+ const warning = result.status === "pending"
1126
+ || result.status === "unsupported"
1127
+ || result.status === "failed";
1128
+ ctx.ui?.notify?.(result.message, warning ? "warning" : "info");
1129
+ },
1088
1130
  stopAgent: async (name: string) => {
1089
1131
  const targetTeamName = teamName;
1090
1132
  if (!targetTeamName) return;
@@ -1221,6 +1263,7 @@ export default function (pi: ExtensionAPI) {
1221
1263
  terminal,
1222
1264
  runningReadAgents,
1223
1265
  readAgentKey,
1266
+ interruptTeammate,
1224
1267
  shutdownTeammate,
1225
1268
  getTeamName: () => teamName,
1226
1269
  });
@@ -9,7 +9,7 @@ export const NESTED_READ_MODEL_SLOTS = [
9
9
 
10
10
  export type NestedReadModelSlot = (typeof NESTED_READ_MODEL_SLOTS)[number];
11
11
 
12
- export const NESTED_DELEGATION_TOOL_NAMES = ["spawn_agent", "spawn_swarm_agents"] as const;
12
+ export const NESTED_DELEGATION_TOOL_NAMES = ["get_agent_status", "spawn_agent", "spawn_swarm_agents"] as const;
13
13
 
14
14
  const ELIGIBLE_PARENT_MODEL_SLOTS = new Set(["write-feature", "write-critical"]);
15
15