@melihmucuk/pi-crew 1.0.1 → 1.0.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.
package/README.md CHANGED
@@ -68,12 +68,12 @@ Closes an interactive subagent session owned by the current session when you no
68
68
  "close planner-a1b2, the plan looks good"
69
69
  ```
70
70
 
71
- ### `/pi-crew:abort`
71
+ ### `/pi-crew-abort`
72
72
 
73
73
  Aborts a running subagent. Supports tab completion for subagent IDs.
74
74
  Unlike the `crew_abort` tool, this command is intentionally unrestricted and works as an emergency escape hatch across sessions.
75
75
 
76
- ### `/pi-crew:review`
76
+ ### `/pi-crew-review`
77
77
 
78
78
  Expands a bundled prompt template that orchestrates parallel code and quality reviews.
79
79
  Use it to review recent commits, staged changes, unstaged changes, and untracked files with `code-reviewer` and `quality-reviewer`, then merge both results into one report.
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import type { AgentConfig } from "./agent-discovery.js";
3
3
  import { type AbortableAgentSummary, type ActiveAgentSummary } from "./runtime/subagent-state.js";
4
- export type { AbortableAgentSummary, ActiveAgentSummary } from "./runtime/subagent-state.js";
4
+ export type { AbortableAgentSummary, ActiveAgentSummary, } from "./runtime/subagent-state.js";
5
5
  export interface AbortOwnedResult {
6
6
  abortedIds: string[];
7
7
  missingIds: string[];
@@ -14,6 +14,7 @@ export declare class CrewManager {
14
14
  private extensionResolvedPath;
15
15
  private registry;
16
16
  private delivery;
17
+ private overflowAbortControllers;
17
18
  onWidgetUpdate: (() => void) | undefined;
18
19
  constructor(extensionResolvedPath: string);
19
20
  activateSession(sessionId: string, isIdle: () => boolean, pi: ExtensionAPI): void;
@@ -1,5 +1,6 @@
1
1
  import { bootstrapSession } from "./bootstrap-session.js";
2
2
  import { DeliveryCoordinator } from "./runtime/delivery-coordinator.js";
3
+ import { runPromptWithOverflowRecovery } from "./runtime/overflow-recovery.js";
3
4
  import { SubagentRegistry } from "./runtime/subagent-registry.js";
4
5
  import { isAbortableStatus, isAborted, } from "./runtime/subagent-state.js";
5
6
  function getLastAssistantMessage(messages) {
@@ -46,6 +47,7 @@ export class CrewManager {
46
47
  extensionResolvedPath;
47
48
  registry = new SubagentRegistry();
48
49
  delivery = new DeliveryCoordinator();
50
+ overflowAbortControllers = new Map();
49
51
  onWidgetUpdate;
50
52
  constructor(extensionResolvedPath) {
51
53
  this.extensionResolvedPath = extensionResolvedPath;
@@ -111,11 +113,21 @@ export class CrewManager {
111
113
  async runPromptCycle(state, prompt, pi) {
112
114
  if (isAborted(state))
113
115
  return;
116
+ const abortController = new AbortController();
117
+ this.overflowAbortControllers.set(state.id, abortController);
114
118
  try {
115
- await state.session.prompt(prompt);
119
+ const recovery = await runPromptWithOverflowRecovery(state.session, prompt, abortController.signal);
116
120
  if (isAborted(state))
117
121
  return;
118
122
  const outcome = getPromptOutcome(state);
123
+ // If overflow recovery ran but failed, keep the error from outcome.
124
+ // If it recovered, outcome now reflects the retry turn's result.
125
+ if (recovery === "failed" && outcome.status !== "error") {
126
+ this.settleAgent(state, "error", {
127
+ error: "Context overflow recovery failed",
128
+ }, pi);
129
+ return;
130
+ }
119
131
  this.settleAgent(state, outcome.status, outcome, pi);
120
132
  }
121
133
  catch (err) {
@@ -124,6 +136,9 @@ export class CrewManager {
124
136
  const error = err instanceof Error ? err.message : String(err);
125
137
  this.settleAgent(state, "error", { error }, pi);
126
138
  }
139
+ finally {
140
+ this.overflowAbortControllers.delete(state.id);
141
+ }
127
142
  }
128
143
  async spawnSession(state, cwd, parentSessionFile, ctx, pi) {
129
144
  try {
@@ -159,7 +174,9 @@ export class CrewManager {
159
174
  return { error: `Subagent "${id}" belongs to a different session` };
160
175
  }
161
176
  if (state.status !== "waiting") {
162
- return { error: `Subagent "${id}" is not waiting for a response (status: ${state.status})` };
177
+ return {
178
+ error: `Subagent "${id}" is not waiting for a response (status: ${state.status})`,
179
+ };
163
180
  }
164
181
  if (!state.session)
165
182
  return { error: `Subagent "${id}" has no active session` };
@@ -185,6 +202,10 @@ export class CrewManager {
185
202
  const state = this.registry.get(id);
186
203
  if (!state || !isAbortableStatus(state.status))
187
204
  return false;
205
+ this.overflowAbortControllers.get(id)?.abort();
206
+ this.overflowAbortControllers.delete(id);
207
+ state.session?.abortCompaction();
208
+ state.session?.abortRetry();
188
209
  state.session?.abort().catch(() => { });
189
210
  this.settleAgent(state, "aborted", { error: opts.reason }, pi);
190
211
  return true;
@@ -223,7 +244,9 @@ export class CrewManager {
223
244
  return ids;
224
245
  }
225
246
  abortForOwner(ownerSessionId, pi) {
226
- this.abortAllOwned(ownerSessionId, pi, { reason: "Aborted on session shutdown" });
247
+ this.abortAllOwned(ownerSessionId, pi, {
248
+ reason: "Aborted on session shutdown",
249
+ });
227
250
  this.delivery.clearPendingForOwner(ownerSessionId);
228
251
  }
229
252
  getAbortableAgents() {
@@ -1,5 +1,5 @@
1
1
  export function registerCrewCommand(pi, crewManager) {
2
- pi.registerCommand("pi-crew:abort", {
2
+ pi.registerCommand("pi-crew-abort", {
3
3
  description: "Abort an active subagent",
4
4
  getArgumentCompletions(argumentPrefix) {
5
5
  const activeAgents = crewManager.getAbortableAgents();
@@ -5,7 +5,7 @@ export function registerCrewSpawnTool({ pi, crewManager, notifyDiscoveryWarnings
5
5
  pi.registerTool({
6
6
  name: "crew_spawn",
7
7
  label: "Spawn Crew",
8
- description: "Spawn a non-blocking subagent that runs in an isolated session. The subagent works independently while the current session stays interactive. Results are delivered back to the spawning session as steering messages when done. Use crew_list first to see available subagents.",
8
+ description: "Spawn a non-blocking subagent that runs in an isolated session. The subagent works independently while your session stays interactive. Results are delivered back to your session as steering messages when done. NEVER PREDICT or FABRICATE results for subagents that have not yet reported back to you. Use crew_list first to see available subagents.",
9
9
  parameters: Type.Object({
10
10
  subagent: Type.String({ description: "Subagent name from crew_list" }),
11
11
  task: Type.String({ description: "Task to delegate to the subagent" }),
@@ -14,10 +14,10 @@ export function registerCrewSpawnTool({ pi, crewManager, notifyDiscoveryWarnings
14
14
  promptGuidelines: [
15
15
  "Use crew_* tools to delegate parallelizable, independent tasks to specialized subagents. For interactive multi-turn workflows, use crew_respond/crew_done. Avoid spawning for trivial, single-turn tasks.",
16
16
  "crew_spawn: Always call crew_list first to see which subagents are available before spawning.",
17
- "crew_spawn: The spawned subagent runs in a separate context window with no access to the current conversation. Include all relevant context (file paths, requirements, prior findings) directly in the task parameter.",
17
+ "crew_spawn: The spawned subagent runs in a separate context window with no access to your session. Include all relevant context (file paths, requirements, prior findings) directly in the task parameter.",
18
18
  "crew_spawn: Results are delivered asynchronously as steering messages. Do not block or poll for completion. If there are other independent tasks to handle, continue with those; otherwise wait for the user's next instruction or the subagent result.",
19
19
  "crew_spawn: NEVER perform the same work you delegated to a subagent. Once a task is spawned, trust the subagent to do it. Do not run the same searches, reads, or analysis yourself while waiting. You may only gather context BEFORE spawning to prepare the task description. After spawning, move on to other independent work or simply wait for the result.",
20
- "crew_spawn: When multiple subagents are spawned, each result arrives as a separate steering message. NEVER predict or fabricate results for subagents that have not yet reported back. Wait for ALL crew-result messages.",
20
+ "crew_spawn: When multiple subagents are spawned, each result arrives as a separate steering message. NEVER PREDICT or FABRICATE results for subagents that have not yet reported back to you. Wait for ALL crew-result messages.",
21
21
  "crew_spawn: Interactive subagents (marked with 'interactive' in crew_list) stay alive after responding. Use crew_respond to continue the conversation and crew_done to close when finished.",
22
22
  ],
23
23
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -0,0 +1,3 @@
1
+ import type { AgentSession } from "@mariozechner/pi-coding-agent";
2
+ export type OverflowRecoveryResult = "none" | "recovered" | "failed";
3
+ export declare function runPromptWithOverflowRecovery(session: AgentSession, text: string, signal: AbortSignal): Promise<OverflowRecoveryResult>;
@@ -0,0 +1,155 @@
1
+ const OVERFLOW_RECOVERY_TIMEOUT_MS = 120_000;
2
+ /**
3
+ * Short grace period for the first terminal agent_end after prompt() resolves.
4
+ * If this window expires, we still wait the full recovery timeout.
5
+ */
6
+ const INITIAL_AGENT_END_WAIT_MS = 5_000;
7
+ function createDeferredPhase() {
8
+ let done = false;
9
+ let resolveFn;
10
+ const promise = new Promise((resolve) => {
11
+ resolveFn = () => {
12
+ if (done)
13
+ return;
14
+ done = true;
15
+ resolve();
16
+ };
17
+ });
18
+ return {
19
+ promise,
20
+ resolve: () => resolveFn?.(),
21
+ isDone: () => done,
22
+ };
23
+ }
24
+ class OverflowRecoveryTracker {
25
+ overflowDetected = false;
26
+ compactionWillRetry = false;
27
+ autoRetryActive = false;
28
+ initialAgentEnd = createDeferredPhase();
29
+ compactionEnd;
30
+ retryAgentEnd;
31
+ overflowAutoRetryEnd;
32
+ timers = [];
33
+ handleEvent(event) {
34
+ switch (event.type) {
35
+ case "agent_end":
36
+ this.onAgentEnd();
37
+ break;
38
+ case "compaction_start":
39
+ this.onCompactionStart(event.reason);
40
+ break;
41
+ case "compaction_end":
42
+ this.onCompactionEnd(event.reason, event.willRetry);
43
+ break;
44
+ case "auto_retry_start":
45
+ this.onAutoRetryStart();
46
+ break;
47
+ case "auto_retry_end":
48
+ this.onAutoRetryEnd();
49
+ break;
50
+ default:
51
+ break;
52
+ }
53
+ }
54
+ async awaitCompletion(signal) {
55
+ const cancelPromise = new Promise((resolve) => {
56
+ if (signal.aborted) {
57
+ resolve();
58
+ return;
59
+ }
60
+ signal.addEventListener("abort", () => resolve(), { once: true });
61
+ });
62
+ try {
63
+ let initialEnd = await this.waitForPhase(this.initialAgentEnd.promise, INITIAL_AGENT_END_WAIT_MS, cancelPromise);
64
+ if (initialEnd === "timeout") {
65
+ initialEnd = await this.waitForPhase(this.initialAgentEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
66
+ }
67
+ if (initialEnd !== "done") {
68
+ return this.overflowDetected ? "failed" : "none";
69
+ }
70
+ if (!this.overflowDetected)
71
+ return "none";
72
+ if (this.compactionEnd) {
73
+ const compactionEnd = await this.waitForPhase(this.compactionEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
74
+ if (compactionEnd !== "done")
75
+ return "failed";
76
+ }
77
+ if (!this.compactionWillRetry)
78
+ return "failed";
79
+ if (this.retryAgentEnd) {
80
+ const retryEnd = await this.waitForPhase(this.retryAgentEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
81
+ if (retryEnd !== "done")
82
+ return "failed";
83
+ }
84
+ if (this.overflowAutoRetryEnd) {
85
+ const autoRetryEnd = await this.waitForPhase(this.overflowAutoRetryEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
86
+ if (autoRetryEnd !== "done")
87
+ return "failed";
88
+ }
89
+ return "recovered";
90
+ }
91
+ finally {
92
+ for (const timer of this.timers)
93
+ clearTimeout(timer);
94
+ }
95
+ }
96
+ async waitForPhase(phasePromise, timeoutMs, cancelPromise) {
97
+ return Promise.race([
98
+ phasePromise.then(() => "done"),
99
+ cancelPromise.then(() => "cancelled"),
100
+ new Promise((resolve) => {
101
+ this.timers.push(setTimeout(() => resolve("timeout"), timeoutMs));
102
+ }),
103
+ ]);
104
+ }
105
+ // agent_end can be followed immediately by auto_retry_start in the same
106
+ // _processAgentEvent tick. Resolve on microtask so we can ignore retrying
107
+ // attempts and only accept terminal agent_end events.
108
+ onAgentEnd() {
109
+ queueMicrotask(() => {
110
+ if (this.autoRetryActive)
111
+ return;
112
+ if (!this.initialAgentEnd.isDone()) {
113
+ this.initialAgentEnd.resolve();
114
+ return;
115
+ }
116
+ this.retryAgentEnd?.resolve();
117
+ });
118
+ }
119
+ onCompactionStart(reason) {
120
+ if (reason !== "overflow")
121
+ return;
122
+ this.overflowDetected = true;
123
+ this.compactionEnd ??= createDeferredPhase();
124
+ }
125
+ onCompactionEnd(reason, willRetry) {
126
+ if (reason !== "overflow")
127
+ return;
128
+ this.compactionWillRetry = willRetry;
129
+ if (willRetry) {
130
+ this.retryAgentEnd ??= createDeferredPhase();
131
+ }
132
+ this.compactionEnd?.resolve();
133
+ }
134
+ onAutoRetryStart() {
135
+ this.autoRetryActive = true;
136
+ if (this.overflowDetected) {
137
+ this.overflowAutoRetryEnd ??= createDeferredPhase();
138
+ }
139
+ }
140
+ onAutoRetryEnd() {
141
+ this.autoRetryActive = false;
142
+ this.overflowAutoRetryEnd?.resolve();
143
+ }
144
+ }
145
+ export async function runPromptWithOverflowRecovery(session, text, signal) {
146
+ const tracker = new OverflowRecoveryTracker();
147
+ const unsubscribe = session.subscribe((event) => tracker.handleEvent(event));
148
+ try {
149
+ await session.prompt(text);
150
+ return await tracker.awaitCompletion(signal);
151
+ }
152
+ finally {
153
+ unsubscribe();
154
+ }
155
+ }
@@ -199,7 +199,7 @@ File:
199
199
 
200
200
  - `extension/integration/register-command.ts`
201
201
 
202
- The extension also registers the `/pi-crew:abort` command.
202
+ The extension also registers the `/pi-crew-abort` command.
203
203
 
204
204
  This command differs from `crew_abort` in one important way:
205
205
 
@@ -616,7 +616,7 @@ That last point is essential. The latest subagent response was already delivered
616
616
  There are three conceptually different abort sources:
617
617
 
618
618
  1. tool-triggered aborts through `crew_abort`
619
- 2. unrestricted manual aborts through `/pi-crew:abort`
619
+ 2. unrestricted manual aborts through `/pi-crew-abort`
620
620
  3. cleanup aborts when an owner session shuts down
621
621
 
622
622
  Each path should report the real reason.
@@ -648,7 +648,7 @@ Relevant file:
648
648
 
649
649
  - `extension/integration/register-command.ts`
650
650
 
651
- `/pi-crew:abort` can target any active abortable subagent, regardless of owner. This is not a bug. It is an explicit operational decision.
651
+ `/pi-crew-abort` can target any active abortable subagent, regardless of owner. This is not a bug. It is an explicit operational decision.
652
652
 
653
653
  ### 11.4 Session shutdown cleanup
654
654
 
@@ -691,7 +691,7 @@ This prevents cross-session interference in normal tool-driven workflows.
691
691
 
692
692
  ### 12.3 What is intentionally not isolated
693
693
 
694
- The emergency command `/pi-crew:abort` is intentionally cross-session.
694
+ The emergency command `/pi-crew-abort` is intentionally cross-session.
695
695
 
696
696
  This is the only major exception to normal ownership isolation.
697
697
 
@@ -752,7 +752,7 @@ Architecturally, these files are not special-cased by the runtime. They are auto
752
752
 
753
753
  File:
754
754
 
755
- - `prompts/pi-crew:review.md`
755
+ - `prompts/pi-crew-review.md`
756
756
 
757
757
  This prompt template is a good example of how `pi-crew` is meant to be consumed by higher-level orchestration prompts.
758
758
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melihmucuk/pi-crew",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "Non-blocking subagent orchestration for pi coding agent",
6
6
  "files": [
@@ -166,3 +166,9 @@ Overall assessment: [short clear assessment]
166
166
  - sort by severity
167
167
  - no unnecessary introduction
168
168
  - review only, no code changes
169
+
170
+ ## IMPORTANT
171
+
172
+ - DO NOT perform any code review or quality review analysis yourself.
173
+ - SPAWN the subagents with the review context and WAIT for their results.
174
+ - NEVER PREDICT or FABRICATE results for subagents that have not yet reported back to you.