@melihmucuk/pi-crew 1.0.16 → 1.0.17

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 (35) hide show
  1. package/README.md +8 -8
  2. package/agents/code-reviewer.md +2 -2
  3. package/agents/oracle.md +1 -1
  4. package/agents/planner.md +5 -1
  5. package/agents/quality-reviewer.md +2 -2
  6. package/agents/scout.md +2 -2
  7. package/agents/worker.md +3 -3
  8. package/extension/agent-catalog.ts +369 -0
  9. package/extension/agent-config-fields.ts +359 -0
  10. package/extension/agent-discovery.ts +49 -717
  11. package/extension/index.ts +4 -2
  12. package/extension/integration/crew-tool-actions.ts +306 -0
  13. package/extension/integration/crew-tool-executor.ts +109 -0
  14. package/extension/integration/register-tools.ts +10 -2
  15. package/extension/integration/tool-presentation.ts +0 -20
  16. package/extension/integration/tools/crew-abort.ts +14 -84
  17. package/extension/integration/tools/crew-done.ts +7 -26
  18. package/extension/integration/tools/crew-list.ts +4 -60
  19. package/extension/integration/tools/crew-respond.ts +8 -29
  20. package/extension/integration/tools/crew-spawn.ts +15 -56
  21. package/extension/message-delivery-policy.ts +22 -0
  22. package/extension/runtime/crew-runtime.ts +60 -223
  23. package/extension/runtime/{delivery-coordinator.ts → owner-session-coordinator.ts} +44 -37
  24. package/extension/runtime/subagent-lifecycle.ts +203 -0
  25. package/extension/runtime/subagent-registry.ts +50 -6
  26. package/extension/runtime/subagent-transitions.ts +100 -0
  27. package/extension/subagent-messages.ts +9 -17
  28. package/package.json +8 -6
  29. package/prompts/pi-crew-plan.md +14 -13
  30. package/prompts/pi-crew-review.md +20 -16
  31. package/skills/pi-crew/REFERENCE.md +32 -20
  32. package/skills/pi-crew/SKILL.md +13 -10
  33. package/extension/integration/tools/tool-deps.ts +0 -16
  34. package/extension/integration.ts +0 -13
  35. package/extension/runtime/subagent-state.ts +0 -59
@@ -1,8 +1,8 @@
1
+ import type { SendMessageFn } from "../message-delivery-policy.js";
1
2
  import {
2
3
  type SteeringPayload,
3
4
  sendRemainingNote,
4
5
  sendSteeringMessage,
5
- type SendMessageFn,
6
6
  } from "../subagent-messages.js";
7
7
 
8
8
  export interface ActiveRuntimeBinding {
@@ -17,25 +17,43 @@ interface PendingMessage {
17
17
  queuedAt: number;
18
18
  }
19
19
 
20
- export class DeliveryCoordinator {
20
+ interface OwnerSessionCoordinatorDeps {
21
+ countRunningForOwner: (ownerSessionId: string, excludeId: string) => number;
22
+ onRefreshOwnerSession: (ownerSessionId: string) => void;
23
+ now?: () => number;
24
+ scheduleFlush?: (callback: () => void) => void;
25
+ }
26
+
27
+ const PENDING_MESSAGE_TTL_MS = 86_400_000;
28
+
29
+ export class OwnerSessionCoordinator {
21
30
  private binding: ActiveRuntimeBinding | undefined;
22
31
  private pendingMessages: PendingMessage[] = [];
23
32
  private flushScheduled = false;
33
+ private readonly countRunningForOwner: (ownerSessionId: string, excludeId: string) => number;
34
+ private readonly onRefreshOwnerSession: (ownerSessionId: string) => void;
35
+ private readonly now: () => number;
36
+ private readonly scheduleFlush: (callback: () => void) => void;
37
+
38
+ constructor(deps: OwnerSessionCoordinatorDeps) {
39
+ this.countRunningForOwner = deps.countRunningForOwner;
40
+ this.onRefreshOwnerSession = deps.onRefreshOwnerSession;
41
+ this.now = deps.now ?? Date.now;
42
+ this.scheduleFlush = deps.scheduleFlush ?? ((callback) => setTimeout(callback, 0));
43
+ }
24
44
 
25
- activateSession(
26
- binding: ActiveRuntimeBinding,
27
- countRunningForOwner: (ownerSessionId: string, excludeId: string) => number,
28
- ): void {
45
+ activateSession(binding: ActiveRuntimeBinding): void {
29
46
  this.binding = binding;
47
+
30
48
  // Delay flush to next macrotask. session_start fires before pi-core
31
49
  // calls _reconnectToAgent(), so synchronous delivery would emit agent
32
50
  // events while the session listener is disconnected, losing JSONL persistence.
33
51
  if (this.pendingMessages.some((entry) => entry.ownerSessionId === binding.sessionId)) {
34
52
  this.flushScheduled = true;
35
- setTimeout(() => {
53
+ this.scheduleFlush(() => {
36
54
  this.flushScheduled = false;
37
- this.flushPending(countRunningForOwner);
38
- }, 0);
55
+ this.flushPending();
56
+ });
39
57
  }
40
58
  }
41
59
 
@@ -45,38 +63,34 @@ export class DeliveryCoordinator {
45
63
  }
46
64
  }
47
65
 
48
- deliver(
49
- ownerSessionId: string,
50
- payload: SteeringPayload,
51
- countRunningForOwner: (ownerSessionId: string, excludeId: string) => number,
52
- ): void {
66
+ refresh(ownerSessionId: string): void {
67
+ this.onRefreshOwnerSession(ownerSessionId);
68
+ }
69
+
70
+ deliver(ownerSessionId: string, payload: SteeringPayload): void {
53
71
  if (!this.binding || ownerSessionId !== this.binding.sessionId || this.flushScheduled) {
54
- this.pendingMessages.push({ ownerSessionId, payload, queuedAt: Date.now() });
72
+ this.queue(ownerSessionId, payload);
55
73
  return;
56
74
  }
57
75
 
58
- this.send(ownerSessionId, payload, countRunningForOwner);
76
+ this.send(ownerSessionId, payload);
77
+ }
78
+
79
+ private queue(ownerSessionId: string, payload: SteeringPayload): void {
80
+ this.pendingMessages.push({ ownerSessionId, payload, queuedAt: this.now() });
59
81
  }
60
82
 
61
- /**
62
- * Remove pending messages older than the TTL.
63
- * Called during activateSession to prevent unbounded memory growth.
64
- */
65
83
  private cleanStaleMessages(): void {
66
- const maxAgeMs = 86_400_000; // 24 hours
67
- const cutoff = Date.now() - maxAgeMs;
84
+ const cutoff = this.now() - PENDING_MESSAGE_TTL_MS;
68
85
  this.pendingMessages = this.pendingMessages.filter(
69
86
  (entry) => entry.queuedAt >= cutoff,
70
87
  );
71
88
  }
72
89
 
73
- private flushPending(
74
- countRunningForOwner: (ownerSessionId: string, excludeId: string) => number,
75
- ): void {
90
+ private flushPending(): void {
76
91
  if (!this.binding) return;
77
92
  const targetSessionId = this.binding.sessionId;
78
93
 
79
- // Clean up stale messages first (older than TTL)
80
94
  this.cleanStaleMessages();
81
95
 
82
96
  const toDeliver: PendingMessage[] = [];
@@ -86,17 +100,14 @@ export class DeliveryCoordinator {
86
100
  if (entry.ownerSessionId === targetSessionId) {
87
101
  toDeliver.push(entry);
88
102
  } else {
89
- // Keep all other messages - they may be for sessions that will be reactivated later
90
103
  remaining.push(entry);
91
104
  }
92
105
  }
93
106
 
94
- // Keep messages for other sessions
95
107
  this.pendingMessages = remaining;
96
108
 
97
- // Deliver messages for the active session
98
109
  for (const entry of toDeliver) {
99
- this.send(entry.ownerSessionId, entry.payload, countRunningForOwner);
110
+ this.send(entry.ownerSessionId, entry.payload);
100
111
  }
101
112
  }
102
113
 
@@ -105,17 +116,13 @@ export class DeliveryCoordinator {
105
116
  * owner is idle, queue the result without triggering, then queue the separate
106
117
  * remaining note with triggerTurn so the next turn sees both in order.
107
118
  */
108
- private send(
109
- ownerSessionId: string,
110
- payload: SteeringPayload,
111
- countRunningForOwner: (ownerSessionId: string, excludeId: string) => number,
112
- ): void {
119
+ private send(ownerSessionId: string, payload: SteeringPayload): void {
113
120
  if (!this.binding || this.binding.sessionId !== ownerSessionId) {
114
- this.pendingMessages.push({ ownerSessionId, payload, queuedAt: Date.now() });
121
+ this.queue(ownerSessionId, payload);
115
122
  return;
116
123
  }
117
124
 
118
- const remaining = countRunningForOwner(ownerSessionId, payload.id);
125
+ const remaining = this.countRunningForOwner(ownerSessionId, payload.id);
119
126
  const isIdle = this.binding.isIdle();
120
127
  const triggerResultTurn = !(isIdle && remaining > 0);
121
128
 
@@ -0,0 +1,203 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
3
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
4
+ import type { BootstrapContext } from "../bootstrap-session.js";
5
+ import { bootstrapSession } from "../bootstrap-session.js";
6
+ import type { SubagentStatus } from "../subagent-messages.js";
7
+ import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
8
+ import type { SubagentState } from "./subagent-registry.js";
9
+ import { isAborted } from "./subagent-transitions.js";
10
+
11
+ interface PromptOutcome {
12
+ status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
13
+ result?: string;
14
+ error?: string;
15
+ }
16
+
17
+ interface StartOptions {
18
+ cwd: string;
19
+ ctx: BootstrapContext;
20
+ extensionResolvedPath: string;
21
+ onWarning?: (message: string) => void;
22
+ }
23
+
24
+ interface SubagentLifecycleCallbacks {
25
+ isCurrent: (state: SubagentState) => boolean;
26
+ onProgress: (ownerSessionId: string) => void;
27
+ onSettled: (
28
+ state: SubagentState,
29
+ status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">,
30
+ outcome: { result?: string; error?: string },
31
+ ) => void;
32
+ }
33
+
34
+ function getLastAssistantMessage(
35
+ messages: AgentMessage[],
36
+ ): AssistantMessage | undefined {
37
+ for (let i = messages.length - 1; i >= 0; i--) {
38
+ const msg = messages[i];
39
+ if (msg.role === "assistant") {
40
+ return msg as AssistantMessage;
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ function getAssistantText(
47
+ message: AssistantMessage | undefined,
48
+ ): string | undefined {
49
+ if (!message) return undefined;
50
+
51
+ const texts: string[] = [];
52
+ for (const part of message.content) {
53
+ if (part.type === "text") {
54
+ texts.push(part.text);
55
+ }
56
+ }
57
+
58
+ return texts.length > 0 ? texts.join("\n") : undefined;
59
+ }
60
+
61
+ function getPromptOutcome(state: SubagentState): PromptOutcome {
62
+ const lastAssistant = getLastAssistantMessage(state.session!.messages);
63
+ const text = getAssistantText(lastAssistant);
64
+
65
+ if (lastAssistant?.stopReason === "error") {
66
+ return {
67
+ status: "error",
68
+ error: lastAssistant.errorMessage ?? text ?? "(no output)",
69
+ };
70
+ }
71
+
72
+ if (lastAssistant?.stopReason === "aborted") {
73
+ return {
74
+ status: "aborted",
75
+ error: lastAssistant.errorMessage ?? text ?? "(no output)",
76
+ };
77
+ }
78
+
79
+ return {
80
+ status: state.agentConfig.interactive ? "waiting" : "done",
81
+ result: text ?? "(no output)",
82
+ };
83
+ }
84
+
85
+ export class SubagentLifecycle {
86
+ constructor(private readonly callbacks: SubagentLifecycleCallbacks) {}
87
+
88
+ start(state: SubagentState, opts: StartOptions): void {
89
+ void this.spawnSession(state, opts);
90
+ }
91
+
92
+ respond(state: SubagentState, message: string): void {
93
+ void this.runPromptCycle(state, message);
94
+ }
95
+
96
+ abortPrompt(state: SubagentState): void {
97
+ state.promptAbortController?.abort();
98
+ state.promptAbortController = undefined;
99
+ state.session?.abortCompaction();
100
+ state.session?.abortRetry();
101
+ state.session?.abort().catch(() => {});
102
+ }
103
+
104
+ private attachSessionListeners(
105
+ state: SubagentState,
106
+ session: AgentSession,
107
+ ): void {
108
+ state.unsubscribe = session.subscribe((event) => {
109
+ if (event.type !== "turn_end") return;
110
+
111
+ state.turns++;
112
+ const msg = event.message;
113
+ if (msg.role === "assistant") {
114
+ const assistantMsg = msg as AssistantMessage;
115
+ state.contextTokens = assistantMsg.usage.totalTokens;
116
+ state.model = assistantMsg.model;
117
+ }
118
+ this.callbacks.onProgress(state.ownerSessionId);
119
+ });
120
+ }
121
+
122
+ private attachSpawnedSession(
123
+ state: SubagentState,
124
+ session: AgentSession,
125
+ ): boolean {
126
+ if (!this.callbacks.isCurrent(state)) {
127
+ session.dispose();
128
+ return false;
129
+ }
130
+
131
+ state.session = session;
132
+ return true;
133
+ }
134
+
135
+ private async runPromptCycle(
136
+ state: SubagentState,
137
+ prompt: string,
138
+ ): Promise<void> {
139
+ if (isAborted(state)) return;
140
+
141
+ const abortController = new AbortController();
142
+ state.promptAbortController = abortController;
143
+
144
+ try {
145
+ const recovery = await runPromptWithOverflowRecovery(
146
+ state.session!,
147
+ prompt,
148
+ abortController.signal,
149
+ );
150
+ if (isAborted(state)) return;
151
+
152
+ const outcome = getPromptOutcome(state);
153
+
154
+ if (recovery === "failed" && outcome.status !== "error") {
155
+ this.callbacks.onSettled(state, "error", {
156
+ error: "Context overflow recovery failed",
157
+ });
158
+ return;
159
+ }
160
+
161
+ this.callbacks.onSettled(state, outcome.status, outcome);
162
+ } catch (err) {
163
+ if (isAborted(state)) return;
164
+
165
+ const error = err instanceof Error ? err.message : String(err);
166
+ this.callbacks.onSettled(state, "error", { error });
167
+ } finally {
168
+ state.promptAbortController = undefined;
169
+ }
170
+ }
171
+
172
+ private async spawnSession(
173
+ state: SubagentState,
174
+ opts: StartOptions,
175
+ ): Promise<void> {
176
+ try {
177
+ if (isAborted(state)) return;
178
+
179
+ const { session, warnings } = await bootstrapSession({
180
+ agentConfig: state.agentConfig,
181
+ cwd: opts.cwd,
182
+ ctx: opts.ctx,
183
+ extensionResolvedPath: opts.extensionResolvedPath,
184
+ });
185
+
186
+ for (const warning of warnings) {
187
+ opts.onWarning?.(warning);
188
+ }
189
+
190
+ if (!this.attachSpawnedSession(state, session)) return;
191
+
192
+ this.attachSessionListeners(state, session);
193
+ await this.runPromptCycle(state, state.task);
194
+ } catch (err) {
195
+ if (isAborted(state)) return;
196
+
197
+ if (state.status === "running") {
198
+ const error = err instanceof Error ? err.message : String(err);
199
+ this.callbacks.onSettled(state, "error", { error });
200
+ }
201
+ }
202
+ }
203
+ }
@@ -1,10 +1,54 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
1
3
  import type { AgentConfig } from "../agent-discovery.js";
2
- import type { ActiveAgentSummary, SubagentState } from "./subagent-state.js";
3
- import {
4
- buildActiveAgentSummary,
5
- generateId,
6
- isAbortableStatus,
7
- } from "./subagent-state.js";
4
+ import type { SubagentStatus } from "../subagent-messages.js";
5
+ import { isAbortableStatus } from "./subagent-transitions.js";
6
+
7
+ export interface SubagentState {
8
+ id: string;
9
+ agentConfig: AgentConfig;
10
+ task: string;
11
+ status: SubagentStatus;
12
+ ownerSessionId: string;
13
+ session: AgentSession | null;
14
+ turns: number;
15
+ contextTokens: number;
16
+ model: string | undefined;
17
+ error?: string;
18
+ result?: string;
19
+ promptAbortController?: AbortController;
20
+ unsubscribe?: () => void;
21
+ }
22
+
23
+ export interface ActiveAgentSummary {
24
+ id: string;
25
+ agentName: string;
26
+ status: SubagentStatus;
27
+ turns: number;
28
+ contextTokens: number;
29
+ model: string | undefined;
30
+ }
31
+
32
+ function generateId(name: string, existingIds: Set<string>): string {
33
+ for (let i = 0; i < 10; i++) {
34
+ const id = `${name}-${randomBytes(4).toString("hex")}`;
35
+ if (!existingIds.has(id)) return id;
36
+ }
37
+ return `${name}-${randomBytes(8).toString("hex")}`;
38
+ }
39
+
40
+ function buildActiveAgentSummary(
41
+ state: SubagentState,
42
+ ): ActiveAgentSummary {
43
+ return {
44
+ id: state.id,
45
+ agentName: state.agentConfig.name,
46
+ status: state.status,
47
+ turns: state.turns,
48
+ contextTokens: state.contextTokens,
49
+ model: state.model,
50
+ };
51
+ }
8
52
 
9
53
  export class SubagentRegistry {
10
54
  private activeAgents = new Map<string, SubagentState>();
@@ -0,0 +1,100 @@
1
+ import type { SubagentStatus } from "../subagent-messages.js";
2
+ import type { SubagentState } from "./subagent-registry.js";
3
+
4
+ export type SettledSubagentStatus = Extract<
5
+ SubagentStatus,
6
+ "done" | "waiting" | "error" | "aborted"
7
+ >;
8
+
9
+ export interface SubagentTransitionOutcome {
10
+ result?: string;
11
+ error?: string;
12
+ }
13
+
14
+ type SubagentTransitionResult =
15
+ | { ok: true; state: SubagentState }
16
+ | { ok: false; error: string };
17
+
18
+ export function isAborted(state: SubagentState): boolean {
19
+ return state.status === "aborted";
20
+ }
21
+
22
+ export function isAbortableStatus(status: SubagentStatus): boolean {
23
+ return status === "running" || status === "waiting";
24
+ }
25
+
26
+ export function canAbortSubagent(
27
+ state: SubagentState | undefined,
28
+ ): state is SubagentState {
29
+ return Boolean(state && isAbortableStatus(state.status));
30
+ }
31
+
32
+ function validateOwnedSubagent(
33
+ state: SubagentState | undefined,
34
+ id: string,
35
+ callerSessionId: string,
36
+ missingMessage: string,
37
+ ): SubagentTransitionResult {
38
+ if (!state) return { ok: false, error: missingMessage };
39
+ if (state.ownerSessionId !== callerSessionId) {
40
+ return { ok: false, error: `Subagent "${id}" belongs to a different session` };
41
+ }
42
+ return { ok: true, state };
43
+ }
44
+
45
+ export function startSubagentResponse(
46
+ state: SubagentState | undefined,
47
+ id: string,
48
+ callerSessionId: string,
49
+ ): SubagentTransitionResult {
50
+ const owned = validateOwnedSubagent(
51
+ state,
52
+ id,
53
+ callerSessionId,
54
+ `No subagent with id "${id}"`,
55
+ );
56
+ if (!owned.ok) return owned;
57
+
58
+ if (owned.state.status !== "waiting") {
59
+ return {
60
+ ok: false,
61
+ error: `Subagent "${id}" is not waiting for a response (status: ${owned.state.status})`,
62
+ };
63
+ }
64
+ if (!owned.state.session) {
65
+ return { ok: false, error: `Subagent "${id}" has no active session` };
66
+ }
67
+
68
+ owned.state.status = "running";
69
+ return owned;
70
+ }
71
+
72
+ export function validateSubagentDone(
73
+ state: SubagentState | undefined,
74
+ id: string,
75
+ callerSessionId: string,
76
+ ): SubagentTransitionResult {
77
+ const owned = validateOwnedSubagent(
78
+ state,
79
+ id,
80
+ callerSessionId,
81
+ `No active subagent with id "${id}"`,
82
+ );
83
+ if (!owned.ok) return owned;
84
+
85
+ if (owned.state.status !== "waiting") {
86
+ return { ok: false, error: `Subagent "${id}" is not in waiting state` };
87
+ }
88
+
89
+ return owned;
90
+ }
91
+
92
+ export function settleSubagent(
93
+ state: SubagentState,
94
+ status: SettledSubagentStatus,
95
+ outcome: SubagentTransitionOutcome,
96
+ ): void {
97
+ state.status = status;
98
+ state.result = outcome.result;
99
+ state.error = outcome.error;
100
+ }
@@ -1,9 +1,8 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { sendWithDeliveryPolicy, type SendMessageFn } from "./message-delivery-policy.js";
2
2
 
3
+ export type { SendMessageFn } from "./message-delivery-policy.js";
3
4
  export type SubagentStatus = "running" | "waiting" | "done" | "error" | "aborted";
4
5
 
5
- export type SendMessageFn = ExtensionAPI["sendMessage"];
6
-
7
6
  export const STATUS_ICON: Record<SubagentStatus, string> = {
8
7
  running: "⏳",
9
8
  waiting: "⏳",
@@ -79,12 +78,7 @@ export function sendSteeringMessage(
79
78
  } satisfies CrewResultMessageDetails,
80
79
  };
81
80
 
82
- sendMessage(
83
- message,
84
- opts.isIdle
85
- ? { triggerTurn: opts.triggerTurn }
86
- : { deliverAs: "steer", triggerTurn: opts.triggerTurn },
87
- );
81
+ sendWithDeliveryPolicy(message, sendMessage, opts);
88
82
  }
89
83
 
90
84
  export function sendRemainingNote(
@@ -94,15 +88,14 @@ export function sendRemainingNote(
94
88
  ): void {
95
89
  if (remainingCount <= 0) return;
96
90
 
97
- sendMessage(
91
+ sendWithDeliveryPolicy(
98
92
  {
99
93
  customType: "crew-remaining",
100
94
  content: `⏳ ${remainingCount} subagent(s) still running`,
101
95
  display: true,
102
96
  },
103
- opts.isIdle
104
- ? { triggerTurn: opts.triggerTurn }
105
- : { deliverAs: "steer", triggerTurn: opts.triggerTurn },
97
+ sendMessage,
98
+ opts,
106
99
  );
107
100
  }
108
101
 
@@ -110,15 +103,14 @@ export function sendCrewListActiveWarning(
110
103
  sendMessage: SendMessageFn,
111
104
  opts: { isIdle: boolean; triggerTurn: boolean },
112
105
  ): void {
113
- sendMessage(
106
+ sendWithDeliveryPolicy(
114
107
  {
115
108
  customType: "crew-list-warning",
116
109
  content:
117
110
  "⚠ Active subagents detected. Do not poll crew_list for completion — results arrive as steering messages. Continue with unrelated work or end your turn and wait for the steering messages.",
118
111
  display: true,
119
112
  },
120
- opts.isIdle
121
- ? { triggerTurn: opts.triggerTurn }
122
- : { deliverAs: "steer", triggerTurn: opts.triggerTurn },
113
+ sendMessage,
114
+ opts,
123
115
  );
124
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melihmucuk/pi-crew",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "type": "module",
5
5
  "description": "Non-blocking subagent orchestration for pi coding agent",
6
6
  "files": [
@@ -32,7 +32,8 @@
32
32
  "video": "https://monkeys-team.ams3.cdn.digitaloceanspaces.com/pi-crew-demo.mp4"
33
33
  },
34
34
  "scripts": {
35
- "typecheck": "tsc --noEmit"
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "node --import tsx --test tests/**/*.test.ts"
36
37
  },
37
38
  "peerDependencies": {
38
39
  "@earendil-works/pi-agent-core": "*",
@@ -42,11 +43,12 @@
42
43
  "typebox": "*"
43
44
  },
44
45
  "devDependencies": {
45
- "@earendil-works/pi-agent-core": "^0.74.0",
46
- "@earendil-works/pi-ai": "^0.74.0",
47
- "@earendil-works/pi-coding-agent": "^0.74.0",
48
- "@earendil-works/pi-tui": "^0.74.0",
46
+ "@earendil-works/pi-agent-core": "^0.75.4",
47
+ "@earendil-works/pi-ai": "^0.75.4",
48
+ "@earendil-works/pi-coding-agent": "^0.75.4",
49
+ "@earendil-works/pi-tui": "^0.75.4",
49
50
  "@types/node": "^22.19.17",
51
+ "tsx": "^4.22.3",
50
52
  "typebox": "^1.1.38",
51
53
  "typescript": "^5.9.3"
52
54
  }
@@ -6,31 +6,32 @@ description: Orchestrate scouts and planner to produce an implementation plan.
6
6
 
7
7
  Additional instructions: `$ARGUMENTS`
8
8
 
9
- You are a planning orchestrator, not a scout, planner, or implementer. Resolve the task and scope, gather only minimal orientation context, delegate discovery to scouts when available, pass cleaned findings to the planner, and manage the planner lifecycle. Do not perform deep investigation, write the plan yourself, or modify files.
9
+ You are a planning orchestrator, not a scout, planner, or implementer. Resolve the task and scope, gather only minimal task-specific context, delegate discovery to scouts when available, pass cleaned findings to the planner, and manage the planner lifecycle. Do not perform deep investigation, write the plan yourself, or modify files.
10
10
 
11
11
  ## Task and Context
12
12
 
13
13
  Use additional instructions when provided; otherwise use the current conversation task. If the task or scope is decision-critical unclear or conflicting, ask the user before proceeding.
14
14
 
15
- Build shared context for subagents:
15
+ Build compact shared context for subagents. Include only information that helps this planning task beyond the selected subagent’s obvious role:
16
16
 
17
- - user task;
18
- - project root;
19
- - constraints and additional instructions;
20
- - user-provided references as paths/URLs and why they matter;
21
- - scope boundary: in scope, out of scope, assumptions;
22
- - minimal orientation already gathered;
23
- - known stack, dependencies, conventions when relevant.
17
+ - user intent and expected outcome;
18
+ - user-provided references, plus a concise summary after reading them when practical;
19
+ - task-specific decisions, constraints, and assumptions not already covered by repo guidance;
20
+ - non-default scope boundaries, when needed;
21
+ - minimal orientation already gathered, only when it clarifies where to look;
22
+ - exact errors/output or verification context, when relevant.
24
23
 
25
- Do not copy full reference contents. Subagents cannot see conversation context unless you include it.
24
+ Do not copy full reference contents. Do not include project root/cwd, generic repo conventions, default scope, edit permissions, output format, or role boilerplate. Subagents run in the same repo cwd and can inspect repo guidance themselves.
26
25
 
27
- Gather only enough orientation to assign scout scopes or brief the planner: top-level structure, key config, README/AGENTS when relevant, and targeted searches or entrypoint checks. Do not read full files, trace call chains, or analyze implementations.
26
+ If the user provides a plan, spec, issue, doc, design, URL, or file as the source of intent, read it when practical and summarize the relevant intent instead of merely passing the path.
27
+
28
+ Gather only enough orientation to assign scout scopes or brief the planner: targeted searches, likely entry points, and small config or structure checks when they materially affect delegation. Do not read full implementation files, trace call chains, or analyze implementations. Do not read README/AGENTS just to repeat generic repo guidance.
28
29
 
29
30
  ## Scouts
30
31
 
31
32
  Call `crew_list` and check for `scout`. If unavailable, continue to planner with minimal context and note the missing scout coverage.
32
33
 
33
- If available, spawn up to 4 scouts for distinct, non-overlapping focus areas. Keep each task narrow and include shared context, explicit investigation scope, requested facts, read-only constraints, and no build/test/install/format/codegen/server-start commands.
34
+ If available, spawn up to 4 scouts for distinct, non-overlapping focus areas. Keep each task narrow and include only task-specific context, the investigation focus, requested facts, and relevant paths or entry points. Do not restate scout role boilerplate, default read-only behavior, output format, or generic command restrictions.
34
35
 
35
36
  Wait for scout results without polling or fabrication. If a scout fails or returns no useful findings, retry or reformulate once. If it still fails, record the gap and continue.
36
37
 
@@ -40,7 +41,7 @@ Before planner handoff, perform only mechanical cleanup: remove duplicates, irre
40
41
 
41
42
  Call `crew_list` and check for `planner`. If unavailable, tell the user and stop; do not write the plan yourself.
42
43
 
43
- Spawn the planner with shared context, cleaned scout findings, and gaps. The planner is interactive and may return **Blocking Questions**, **Implementation Plan**, or **No plan needed**.
44
+ Spawn the planner with compact shared context, cleaned scout findings, and gaps. Keep the handoff focused on intent, decisions, constraints, facts, paths, relationships, and unresolved questions. Do not restate planner role boilerplate, output format, edit permissions, or obvious next steps.
44
45
 
45
46
  Do not rewrite planner output that is already visible as a steering message.
46
47