allye-pi 1.7.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +73 -310
  2. package/bootstrap/allye.md +30 -0
  3. package/docs/install-omp.md +31 -0
  4. package/docs/install-pi.md +21 -117
  5. package/package.json +10 -4
  6. package/packages/allye-pi/src/index.ts +88 -782
  7. package/skills/bridge/SKILL.md +105 -0
  8. package/skills/bridge/references/crew.md +20 -0
  9. package/skills/bridge/references/delegation.md +174 -0
  10. package/skills/bridge/references/discovery.md +71 -0
  11. package/skills/bridge/references/publish.md +90 -0
  12. package/skills/bridge/references/state-contract.md +109 -0
  13. package/skills/bridge/references/workflow.md +132 -0
  14. package/skills/bridge-architect/SKILL.md +96 -0
  15. package/skills/bridge-architect/references/spec-template.md +90 -0
  16. package/skills/bridge-armorer/SKILL.md +103 -0
  17. package/skills/bridge-copilot/SKILL.md +74 -0
  18. package/skills/bridge-dispatcher/SKILL.md +96 -0
  19. package/skills/bridge-dispatcher/references/spec-template-minimal.md +53 -0
  20. package/skills/bridge-medic/SKILL.md +81 -0
  21. package/skills/bridge-optimizer/SKILL.md +68 -0
  22. package/skills/bridge-pilot/SKILL.md +69 -0
  23. package/skills/bridge-pilot/references/mocking.md +57 -0
  24. package/skills/bridge-pilot/references/tdd.md +42 -0
  25. package/skills/bridge-pilot/references/tests.md +60 -0
  26. package/skills/bridge-recon/SKILL.md +82 -0
  27. package/skills/bridge-shield/SKILL.md +73 -0
  28. package/skills/bridge-strategist/SKILL.md +92 -0
  29. package/skills/bridge-watcher/SKILL.md +73 -0
  30. package/skills/agent-runtime/SKILL.md +0 -129
  31. package/skills/agent-runtime/references/herdr.md +0 -184
  32. package/skills/board-progression/SKILL.md +0 -214
  33. package/skills/branch-landing/SKILL.md +0 -88
  34. package/skills/delivery/SKILL.md +0 -187
  35. package/skills/execution/SKILL.md +0 -396
  36. package/skills/handover-protocol/SKILL.md +0 -74
  37. package/skills/handover-protocol/references/correction.md +0 -32
  38. package/skills/handover-protocol/references/discovery-to-planning.md +0 -44
  39. package/skills/handover-protocol/references/execution-report.md +0 -42
  40. package/skills/handover-protocol/references/planning-to-technical.md +0 -42
  41. package/skills/handover-protocol/references/story-execution.md +0 -74
  42. package/skills/handover-protocol/references/technical-to-orchestration.md +0 -45
  43. package/skills/memory-protocol/SKILL.md +0 -319
  44. package/skills/orchestrator/SKILL.md +0 -249
  45. package/skills/product-planning/SKILL.md +0 -257
  46. package/skills/review/SKILL.md +0 -165
  47. package/skills/sandbox/SKILL.md +0 -62
  48. package/skills/setup/SKILL.md +0 -225
  49. package/skills/tdd-workflow/SKILL.md +0 -144
  50. package/skills/technical-planning/SKILL.md +0 -354
  51. package/skills/tools-quickref/SKILL.md +0 -308
  52. package/skills/using-allye/SKILL.md +0 -171
  53. package/skills/verification-loop/SKILL.md +0 -93
@@ -1,182 +1,74 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
1
  import { dirname, resolve } from "node:path";
4
- import { existsSync, readFileSync } from "node:fs";
2
+ import { readFileSync } from "node:fs";
5
3
  import { fileURLToPath } from "node:url";
6
- import { randomUUID } from "node:crypto";
7
- import { Type } from "typebox";
8
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+
6
+ /**
7
+ * Allye extension for Pi and OMP (oh-my-pi reads the same `pi` manifest).
8
+ *
9
+ * - Exposes the repository's skills/ directory (Bridge) through resources_discover.
10
+ * - Adds the shared bootstrap (bootstrap/allye.md) to the system prompt.
11
+ * - Preloads Allye context through the configured MCP bridge when one is
12
+ * available, and gates team-scoped work until an active team is set.
13
+ */
9
14
 
10
- const execFileAsync = promisify(execFile);
11
15
  const MCP_SERVER = "allye";
12
16
  const MAX_CONTEXT_CHARS = 12_000;
13
- const RUNTIME_TIMEOUT_MS = 30_000;
14
- const AGENT_NAME = /^[a-z][a-z0-9_-]{0,31}$/;
15
17
 
16
- export type AllyeCapabilities = {
17
- piSession: true;
18
- allyeMcp: boolean;
19
- filesystem: true;
20
- subagents: boolean;
21
- herdr: boolean;
22
- };
23
18
  type McpResult = { content?: Array<{ type?: string; text?: string }> };
24
- type StartupContext = {
19
+ export type StartupContext = {
25
20
  text: string;
26
21
  teamSelectionRequired: boolean;
27
22
  allyeUnavailable: boolean;
28
23
  teams: Array<{ id: string; name: string; prefix?: string }>;
29
24
  };
30
- type WaitRegistrar = (name: string, timeoutMs: number, execution?: DelegationExecution) => Record<string, unknown>;
31
- type WaitCanceller = (name: string) => void;
32
- export type WaitOutcome = "completed" | "blocked" | "unknown" | "timeout" | "error" | "aborted";
33
- export type WaitExecutionResult = {
34
- code: number;
35
- killed: boolean;
36
- stdout: string;
37
- stderr: string;
38
- };
39
- export type WaitEvent = {
40
- kind?: "settled" | "intervened";
41
- executionId?: string;
42
- name: string;
43
- timeoutMs: number;
44
- outcome: WaitOutcome;
45
- code?: number;
46
- killed?: boolean;
47
- stdout?: string;
48
- stderr?: string;
49
- error?: string;
50
- timestamp: string;
51
- };
52
- export type WaitEventDelivery = {
53
- appendEntry: (customType: string, event: WaitEvent) => void;
54
- notify?: (message: string, level: "info" | "warning" | "error") => void;
55
- sendMessage?: (
56
- message: { customType: string; content: string; display: boolean; details: WaitEvent },
57
- options: { triggerTurn: true; deliverAs: "followUp" },
58
- ) => void;
59
- };
60
- export type WaitEventDeliveryResult = {
61
- delivered: boolean;
62
- persisted: boolean;
63
- notified: boolean;
64
- sent: boolean;
65
- errors: string[];
66
- };
67
- export type OwnedPane = {
68
- workspaceId: string;
69
- tabId: string;
70
- paneId: string;
71
- agentName?: string;
72
- resourceId?: string;
73
- };
74
- export type OwnedResource = {
75
- resourceId: string;
76
- level: "pane" | "tab" | "workspace";
77
- workspaceId: string;
78
- tabId?: string;
79
- paneId?: string;
80
- createdAt: string;
81
- };
82
- export type DelegationExecution = {
83
- executionId: string;
84
- callerPaneId?: string;
85
- workspaceId: string;
86
- tabId: string;
87
- paneId: string;
88
- resourceLevel: "pane" | "tab" | "workspace";
89
- resourceId?: string;
90
- worktree: string;
91
- agentName?: string;
92
- status: "created" | "working" | "settled" | "blocked" | "unknown" | "intervened" | "cleaned";
93
- runtimeState?: "working" | "idle" | "done" | "blocked" | "unknown";
94
- manualIntervention: boolean;
95
- createdAt: string;
96
- settledAt?: string;
97
- };
98
- export type DelegationState = {
99
- panes: OwnedPane[];
100
- resources: Map<string, OwnedResource>;
101
- waits: Set<string>;
102
- executions: Map<string, DelegationExecution>;
103
- };
104
- const WAIT_EVENT_TYPE = "allye-herdr-wait";
105
- const RUNTIME_TOOL_NAME = "allye_herdr";
106
-
107
- function resultText(result: unknown): string {
108
- const content = (result as McpResult | null)?.content;
109
- if (!Array.isArray(content)) return String(result ?? "");
110
- return content
111
- .filter((part) => part.type === "text" && typeof part.text === "string")
112
- .map((part) => part.text as string)
113
- .join("\n");
114
- }
115
-
116
- function limitText(text: string, maxChars: number): string {
117
- return text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n[context truncated]`;
118
- }
119
-
25
+ type ActiveMcpToolCaller = (
26
+ serverName: string,
27
+ toolName: string,
28
+ args?: Record<string, unknown>,
29
+ signal?: AbortSignal,
30
+ ) => Promise<unknown>;
120
31
  type McpBridgeRuntime = typeof globalThis & {
121
32
  __piMcpAdapterActiveToolCaller?: ActiveMcpToolCaller | null;
122
33
  };
123
34
 
124
- export function describeCapabilities(
125
- env: NodeJS.ProcessEnv = process.env,
126
- mcpAvailable = Boolean((globalThis as McpBridgeRuntime).__piMcpAdapterActiveToolCaller),
127
- ): AllyeCapabilities {
128
- return {
129
- piSession: true,
130
- allyeMcp: env.ALLYE_PI_MCP !== "0" && mcpAvailable,
131
- filesystem: true,
132
- subagents: env.ALLYE_PI_SUBAGENTS === "1",
133
- herdr: env.HERDR_ENV === "1",
134
- };
135
- }
35
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
136
36
 
137
- export function adaptiveInstructions(capabilities: AllyeCapabilities): string {
138
- const available = [
139
- capabilities.allyeMcp ? "Allye/MCP" : "no Allye/MCP",
140
- capabilities.filesystem ? "filesystem" : "no filesystem",
141
- capabilities.subagents ? "subagents" : "no subagents",
142
- capabilities.herdr ? "Herdr" : "no Herdr",
143
- ].join(", ");
144
- return `Allye is an adaptive toolkit, not a mandatory workflow. Available capabilities: ${available}. Use each only when it helps; continue locally when an optional capability is absent. Recommend tasks when they add traceability, delegation, review, or coordination value; if the user explicitly approves working without a task, proceed and verify proportionally. Ask for consent before consequential mutations and keep Allye as the source of truth.`;
37
+ export function skillsPath(): string {
38
+ return resolve(packageRoot, "skills");
145
39
  }
146
40
 
147
- export function loadUsingAllyeSkill(): string {
41
+ export function loadBootstrap(): string {
148
42
  try {
149
- return readFileSync(resolve(canonicalSkillsPath(), "using-allye", "SKILL.md"), "utf8");
43
+ return readFileSync(resolve(packageRoot, "bootstrap", "allye.md"), "utf8");
150
44
  } catch {
151
45
  return "";
152
46
  }
153
47
  }
154
48
 
155
- function canonicalSkillsPath(): string {
156
- // The package lives at packages/allye-pi; skills/ is the repository's one
157
- // canonical source and is intentionally not copied into this adapter.
158
- return resolve(dirname(fileURLToPath(import.meta.url)), "../../../skills");
49
+ function resultText(result: unknown): string {
50
+ const content = (result as McpResult | null)?.content;
51
+ if (!Array.isArray(content)) return String(result ?? "");
52
+ return content
53
+ .filter((part) => part.type === "text" && typeof part.text === "string")
54
+ .map((part) => part.text as string)
55
+ .join("\n");
159
56
  }
160
57
 
161
- type ActiveMcpToolCaller = (
162
- serverName: string,
163
- toolName: string,
164
- args?: Record<string, unknown>,
165
- signal?: AbortSignal,
166
- ) => Promise<unknown>;
58
+ function limitText(text: string, maxChars: number): string {
59
+ return text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n[context truncated]`;
60
+ }
167
61
 
168
- async function configuredMcpCall(toolName: string, args: Record<string, unknown>): Promise<unknown> {
169
- // pi-mcp-adapter intentionally exposes its cooperating-extension bridge on
170
- // the in-process runtime. There is no public callMcpTool export in the
171
- // supported package version, so do not dynamically import its TypeScript
172
- // source from node_modules. A missing bridge is a hard bootstrap failure.
173
- const globalBridge = (globalThis as McpBridgeRuntime).__piMcpAdapterActiveToolCaller;
174
- if (!globalBridge) throw new Error("pi-mcp-adapter is not initialized; start the configured MCP adapter before using Allye");
175
- return globalBridge(MCP_SERVER, toolName, args);
62
+ export function mcpBridgeAvailable(env: NodeJS.ProcessEnv = process.env): boolean {
63
+ return env.ALLYE_PI_MCP !== "0" && Boolean((globalThis as McpBridgeRuntime).__piMcpAdapterActiveToolCaller);
176
64
  }
177
65
 
178
66
  async function callAllye(toolName: string, args: Record<string, unknown>): Promise<string> {
179
- return resultText(await configuredMcpCall(toolName, args));
67
+ // pi-mcp-adapter may expose an in-process bridge for cooperating extensions.
68
+ // Without it, the agent still reaches Allye through its own MCP tools.
69
+ const bridge = (globalThis as McpBridgeRuntime).__piMcpAdapterActiveToolCaller;
70
+ if (!bridge) throw new Error("no in-process MCP bridge is available to preload Allye context");
71
+ return resultText(await bridge(MCP_SERVER, toolName, args));
180
72
  }
181
73
 
182
74
  function parseJsonBlock(text: string): Record<string, unknown> | null {
@@ -190,146 +82,45 @@ function parseJsonBlock(text: string): Record<string, unknown> | null {
190
82
  }
191
83
  }
192
84
 
85
+ export function unavailableStartupContext(reason: string): StartupContext {
86
+ return {
87
+ text: `## Allye context not preloaded\n${reason}\nUse the Allye MCP tools directly when the task needs them: call \`initialize\` first and, if no team is active, ask the user which team to use before any team-scoped call.`,
88
+ teamSelectionRequired: false,
89
+ allyeUnavailable: true,
90
+ teams: [],
91
+ };
92
+ }
93
+
193
94
  export function inspectTeamSelection(initText: string): StartupContext {
194
95
  const payload = parseJsonBlock(initText);
195
96
  const profile = payload?.profile as Record<string, unknown> | undefined;
196
97
  if (!payload || !profile || !Array.isArray(profile.teams)) {
197
- return invalidStartupContext("Allye initialize returned a payload that the Pi adapter could not interpret.");
98
+ return unavailableStartupContext("Allye `initialize` returned a payload this extension could not interpret.");
198
99
  }
199
- const rawTeams = Array.isArray(profile?.teams) ? profile.teams : [];
200
- const teams = rawTeams.flatMap((team) => {
100
+ const teams = profile.teams.flatMap((team) => {
201
101
  if (!team || typeof team !== "object") return [];
202
102
  const value = team as Record<string, unknown>;
203
103
  return typeof value.id === "string" && typeof value.name === "string"
204
104
  ? [{ id: value.id, name: value.name, ...(typeof value.prefix === "string" ? { prefix: value.prefix } : {}) }]
205
105
  : [];
206
106
  });
207
- const activeTeam = profile?.team;
107
+ const activeTeam = profile.team;
208
108
  const hasActiveTeam = Boolean(activeTeam && typeof activeTeam === "object" && typeof (activeTeam as Record<string, unknown>).id === "string");
209
109
  const teamSelectionRequired = teams.length > 1 && !hasActiveTeam;
210
- const instruction = teamSelectionRequired
211
- ? `## Allye team selection required\nThis account has multiple teams and no active team. Do not call team-scoped work_items or intelligence operations yet. Ask the user to choose one of: ${teams.map((team) => `${team.name}${team.prefix ? ` [${team.prefix}]` : ""} (${team.id})`).join(", ")}. Then use allye_team action team_switch with the chosen team_query. Never choose a team silently.`
110
+ const text = teamSelectionRequired
111
+ ? `## Allye team selection required\nThis account has several teams and none is active. Do not call team-scoped projects, epics, specs, tasks or intelligence operations yet. Ask the user to choose one of: ${teams.map((team) => `${team.name}${team.prefix ? ` [${team.prefix}]` : ""} (${team.id})`).join(", ")}. Then call the \`team\` tool with action \`team_switch\` and the chosen \`team_query\`. Never choose a team silently.`
212
112
  : "";
213
- return { text: instruction, teamSelectionRequired, allyeUnavailable: false, teams };
214
- }
215
-
216
- export function classifyWaitResult(result?: WaitExecutionResult, timeoutMs = 0, error?: unknown): WaitOutcome {
217
- const errorText = error instanceof Error ? error.message : String(error ?? "");
218
- if (/abort/i.test(errorText)) return "aborted";
219
- if (/timeout|timed out/i.test(errorText) || Boolean(result?.killed && timeoutMs > 0)) return "timeout";
220
- if (/blocked/i.test(errorText)) return "blocked";
221
- if (/unknown/i.test(errorText)) return "unknown";
222
- const runtimeState = result ? extractAgentLifecycleState(parseJson(result.stdout)) ?? extractAgentLifecycleState(parseJson(result.stderr)) : null;
223
- if (runtimeState === "blocked") return "blocked";
224
- if (runtimeState === "unknown") return "unknown";
225
- if (!result || result.code !== 0) return "error";
226
- return "completed";
227
- }
228
-
229
- export function shouldEmitWaitEvent(shuttingDown: boolean, settledWaits: Set<string>, name: string): boolean {
230
- if (shuttingDown || settledWaits.has(name)) return false;
231
- settledWaits.add(name);
232
- return true;
233
- }
234
-
235
- export function formatWaitEvent(event: WaitEvent): string {
236
- const output = event.stdout?.trim() || "(no stdout)";
237
- const error = event.error || event.stderr?.trim() || "(none)";
238
- const prefix = event.kind === "intervened" ? "Herdr agent intervention detected" : "Herdr wait settled";
239
- return `${prefix} for agent ${event.name}. Outcome: ${event.outcome}. `
240
- + `This is delegation evidence only, not a completion verdict. `
241
- + `Use allye_herdr collect when a managed work item exists: read work_children for the story and search Allye memories for Review and Implementation evidence before declaring completion.\n\n`
242
- + `timeout_ms=${event.timeoutMs} code=${event.code ?? "n/a"} killed=${event.killed ?? false} timestamp=${event.timestamp}\n`
243
- + `stdout:\n${output}\nerror/stderr:\n${error}`;
244
- }
245
-
246
- export function waitEventNotificationLevel(event: WaitEvent): "info" | "warning" | "error" {
247
- if (event.outcome === "error" || event.outcome === "blocked" || event.outcome === "unknown") return "error";
248
- if (event.outcome === "timeout") return "warning";
249
- return "info";
250
- }
251
-
252
- export function deliverWaitEvent(
253
- event: WaitEvent,
254
- shuttingDown: boolean,
255
- settledWaits: Set<string>,
256
- delivery: WaitEventDelivery,
257
- ): WaitEventDeliveryResult {
258
- const empty: WaitEventDeliveryResult = {
259
- delivered: false,
260
- persisted: false,
261
- notified: false,
262
- sent: false,
263
- errors: [],
264
- };
265
- if (!shouldEmitWaitEvent(shuttingDown, settledWaits, event.name)) return empty;
266
-
267
- const message = formatWaitEvent(event);
268
- const result: WaitEventDeliveryResult = { ...empty, delivered: true };
269
- try {
270
- delivery.appendEntry("allye-runtime-wait", event);
271
- result.persisted = true;
272
- } catch (error) {
273
- result.errors.push(`appendEntry: ${error instanceof Error ? error.message : String(error)}`);
274
- }
275
- if (delivery.notify) {
276
- try {
277
- delivery.notify(message, waitEventNotificationLevel(event));
278
- result.notified = true;
279
- } catch (error) {
280
- result.errors.push(`notify: ${error instanceof Error ? error.message : String(error)}`);
281
- }
282
- }
283
- if (delivery.sendMessage) {
284
- try {
285
- delivery.sendMessage(
286
- { customType: WAIT_EVENT_TYPE, content: message, display: true, details: event },
287
- { triggerTurn: true, deliverAs: "followUp" },
288
- );
289
- result.sent = true;
290
- } catch (error) {
291
- result.errors.push(`sendMessage: ${error instanceof Error ? error.message : String(error)}`);
292
- }
293
- }
294
- return result;
295
- }
296
-
297
- export function invalidStartupContext(message: string): StartupContext {
298
- return {
299
- text: `## Allye optional context unavailable\n${message}\nDo not call team-scoped work_items or intelligence operations until Allye connectivity and team context are available. Continue with local, non-team-scoped work when appropriate.`,
300
- teamSelectionRequired: false,
301
- allyeUnavailable: true,
302
- teams: [],
303
- };
113
+ return { text, teamSelectionRequired, allyeUnavailable: false, teams };
304
114
  }
305
115
 
306
- export function canUseOwnedPane(state: DelegationState, paneId: string): boolean {
307
- return state.panes.some((pane) => pane.paneId === paneId);
308
- }
309
-
310
- async function loadStartupContext(allyeMcp = true): Promise<StartupContext> {
311
- if (!allyeMcp) return invalidStartupContext("Allye/MCP is disabled for this Pi session.");
312
- const sections: string[] = [];
116
+ async function loadStartupContext(): Promise<StartupContext> {
117
+ if (!mcpBridgeAvailable()) return unavailableStartupContext("No in-process MCP bridge is available in this session.");
313
118
  try {
314
- const initialization = await callAllye("allye_initialize", {
315
- action: "init",
316
- include_user_docs: true,
317
- });
119
+ const initialization = await callAllye("initialize", { action: "init", include_user_docs: true });
318
120
  const teamState = inspectTeamSelection(initialization);
319
- sections.push(initialization, teamState.text);
320
- if (!teamState.allyeUnavailable) {
321
- sections.push(await callAllye("allye_intelligence", {
322
- action: "memory_preferences",
323
- }));
324
- }
325
- if (!teamState.teamSelectionRequired && !teamState.allyeUnavailable) {
326
- sections.push(await callAllye("allye_intelligence", {
327
- action: "memory_search",
328
- query: "Pi Allye adapter workflow context",
329
- limit: 5,
330
- return_content: true,
331
- }));
332
- }
121
+ if (teamState.allyeUnavailable) return teamState;
122
+ const sections = [initialization, teamState.text];
123
+ sections.push(await callAllye("intelligence", { action: "memory_preferences" }));
333
124
  return {
334
125
  text: sections.filter(Boolean).map((section) => limitText(section, MAX_CONTEXT_CHARS)).join("\n\n"),
335
126
  teamSelectionRequired: teamState.teamSelectionRequired,
@@ -337,13 +128,13 @@ async function loadStartupContext(allyeMcp = true): Promise<StartupContext> {
337
128
  teams: teamState.teams,
338
129
  };
339
130
  } catch (error) {
340
- return invalidStartupContext(`Allye is unavailable or initialize returned an unreadable payload: ${error instanceof Error ? error.message : String(error)}`);
131
+ return unavailableStartupContext(`Allye context could not be preloaded: ${error instanceof Error ? error.message : String(error)}`);
341
132
  }
342
133
  }
343
134
 
344
135
  async function loadPromptContext(prompt: string): Promise<string> {
345
136
  try {
346
- return limitText(await callAllye("allye_intelligence", {
137
+ return limitText(await callAllye("intelligence", {
347
138
  action: "memory_search",
348
139
  query: prompt,
349
140
  limit: 5,
@@ -354,532 +145,49 @@ async function loadPromptContext(prompt: string): Promise<string> {
354
145
  }
355
146
  }
356
147
 
357
- function toolkitInstructions(): string {
358
- return `## Allye Pi toolkit
359
- ${adaptiveInstructions(describeCapabilities())}
360
- - Use canonical Allye skills as composable playbooks and choose the smallest useful next step for the user's intent.
361
- - Discovery and Product Planning may produce research, documents, memories, or proposed work items; do not create work items until the user approves the proposal.
362
- - Tasks are recommended for meaningful, delegated, multi-step, or review-heavy work, not universally required. If the user approves a no-task path, record the choice when useful and verify the result.
363
- - Use subagents when available and beneficial. Use Herdr only when available and beneficial; never require either one.
364
- - Use Allye for durable context, decisions, work items, and verification evidence. Ask before consequential mutations.`;
365
- }
366
-
367
- function sleep(ms: number): Promise<void> {
368
- return new Promise((resolve) => setTimeout(resolve, ms));
369
- }
370
-
371
- async function runHerdr(args: string[], timeout = RUNTIME_TIMEOUT_MS): Promise<string> {
372
- const result = await execFileAsync("herdr", args, {
373
- timeout,
374
- maxBuffer: 512 * 1024,
375
- env: process.env,
376
- });
377
- return result.stdout || result.stderr || "";
378
- }
379
-
380
- function parseJson(text: string): Record<string, unknown> | null {
381
- try {
382
- const parsed: unknown = JSON.parse(text);
383
- return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null;
384
- } catch {
385
- return null;
386
- }
387
- }
388
-
389
- function herdrResult(text: string): unknown {
390
- const json = parseJson(text);
391
- return json?.result ?? json ?? text.trim();
392
- }
393
-
394
- function collectValues(value: unknown, key: string, output: string[] = []): string[] {
395
- if (!value || typeof value !== "object") return output;
396
- if (Array.isArray(value)) {
397
- for (const item of value) collectValues(item, key, output);
398
- return output;
399
- }
400
- const object = value as Record<string, unknown>;
401
- if (typeof object[key] === "string") output.push(object[key] as string);
402
- for (const child of Object.values(object)) collectValues(child, key, output);
403
- return output;
404
- }
405
-
406
- export function collectPaneRecords(value: unknown, output: OwnedPane[] = []): OwnedPane[] {
407
- if (!value || typeof value !== "object") return output;
408
- if (Array.isArray(value)) {
409
- for (const item of value) collectPaneRecords(item, output);
410
- return output;
411
- }
412
- const object = value as Record<string, unknown>;
413
- const layout = object.layout && typeof object.layout === "object" ? object.layout as Record<string, unknown> : object;
414
- const workspaceId = typeof layout.workspace_id === "string" ? layout.workspace_id : undefined;
415
- const tabId = typeof layout.tab_id === "string" ? layout.tab_id : undefined;
416
- const panes = Array.isArray(layout.panes) ? layout.panes : [];
417
- if (workspaceId && tabId) {
418
- for (const pane of panes) {
419
- if (pane && typeof pane === "object" && typeof (pane as Record<string, unknown>).pane_id === "string") {
420
- output.push({ workspaceId, tabId, paneId: (pane as Record<string, unknown>).pane_id as string });
421
- }
422
- }
423
- return output;
424
- }
425
- for (const child of Object.values(object)) collectPaneRecords(child, output);
426
- return output;
427
- }
428
-
429
- async function waitForInteractiveShell(pane: string): Promise<void> {
430
- const shellNames = new Set(["bash", "zsh", "sh", "fish"]);
431
- for (let attempt = 0; attempt < 10; attempt += 1) {
432
- try {
433
- const info = herdrResult(await runHerdr(["pane", "process-info", "--pane", pane], 5_000));
434
- const foreground = collectValues(info, "name")[0];
435
- if (foreground && shellNames.has(foreground)) return;
436
- } catch {
437
- // The pane may still be initializing; the bounded loop below is the gate.
438
- }
439
- if (attempt < 9) await sleep(3_000);
440
- }
441
- throw new Error(`Herdr pane ${pane} did not reach an interactive shell within 30 seconds`);
442
- }
443
-
444
- export function activeToolsForCapabilities(activeTools: string[], capabilities: AllyeCapabilities): string[] {
445
- const next = new Set(activeTools);
446
- if (capabilities.herdr) next.add(RUNTIME_TOOL_NAME);
447
- else next.delete(RUNTIME_TOOL_NAME);
448
- return [...next];
449
- }
450
-
451
- export type AgentLifecycleState = "working" | "idle" | "done" | "blocked" | "unknown";
452
-
453
- export function extractAgentLifecycleState(value: unknown): AgentLifecycleState | null {
454
- if (!value || typeof value !== "object") return null;
455
- if (Array.isArray(value)) {
456
- for (const item of value) {
457
- const state = extractAgentLifecycleState(item);
458
- if (state) return state;
459
- }
460
- return null;
461
- }
462
- const object = value as Record<string, unknown>;
463
- const raw = object.agent_status;
464
- if (raw === "working" || raw === "idle" || raw === "done" || raw === "blocked" || raw === "unknown") return raw;
465
- for (const child of Object.values(object)) {
466
- const state = extractAgentLifecycleState(child);
467
- if (state) return state;
468
- }
469
- return null;
470
- }
471
-
472
- export function dispatchStateIsAcceptable(stateText: string): boolean {
473
- const state = extractAgentLifecycleState(parseJson(stateText));
474
- return state === "working" || state === "idle" || state === "done";
475
- }
476
-
477
- function requireHerdr(capabilities: AllyeCapabilities): void {
478
- if (!capabilities.herdr) throw new Error("Herdr is not available in this Pi session; use another available capability or continue locally.");
479
- }
480
-
481
- function requireAbsolutePath(value: unknown, name: string): string {
482
- if (typeof value !== "string" || !value.startsWith("/")) {
483
- throw new Error(`${name} must be an absolute path`);
484
- }
485
- return value;
486
- }
487
-
488
- async function cleanupExecution(execution: DelegationExecution, ownership: DelegationState): Promise<unknown> {
489
- if (execution.status === "working" || execution.status === "blocked" || execution.status === "unknown" || execution.manualIntervention) {
490
- throw new Error(`execution ${execution.executionId} is not safe to clean up while status=${execution.status}`);
491
- }
492
- const resourceId = execution.resourceId;
493
- if (!resourceId) return { cleaned: false, reason: "execution has no owned resource" };
494
- const resource = ownership.resources.get(resourceId);
495
- if (!resource) return { cleaned: false, reason: "owned resource is already absent" };
496
- if (resource.level === "pane" && resource.paneId) await runHerdr(["pane", "close", resource.paneId]);
497
- else if (resource.level === "tab" && resource.tabId) await runHerdr(["tab", "close", resource.tabId]);
498
- else if (resource.level === "workspace") await runHerdr(["workspace", "close", resource.workspaceId]);
499
- execution.status = "cleaned";
500
- ownership.resources.delete(resourceId);
501
- ownership.panes = ownership.panes.filter((pane) => pane.resourceId !== resourceId);
502
- return { cleaned: true, resource };
503
- }
504
-
505
- async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<string, unknown>, registerWait: WaitRegistrar, cancelWait: WaitCanceller, ownership: DelegationState): Promise<unknown> {
506
- const operation = params.operation;
507
- if (operation === "detect") {
508
- if (process.env.HERDR_ENV !== "1") return { available: false, reason: "HERDR_ENV is not 1" };
509
- try {
510
- const output = await runHerdr(["status"]);
511
- return { available: /compatible:\s*yes/i.test(output), output: output.trim() };
512
- } catch (error) {
513
- return { available: false, reason: error instanceof Error ? error.message : String(error) };
514
- }
515
- }
516
-
517
- requireHerdr(capabilities);
518
-
519
- if (operation === "workspace") {
520
- const cwd = requireAbsolutePath(params.cwd, "cwd");
521
- if (typeof params.worktree === "string" && params.worktree !== cwd) throw new Error("workspace cwd must match the requested worktree; use worktree management before creating a workspace");
522
- const label = typeof params.label === "string" && params.label.trim() ? params.label.trim() : `allye-${randomUUID().slice(0, 8)}`;
523
- const focus = params.focus === true ? "--focus" : "--no-focus";
524
- const resource = herdrResult(await runHerdr(["workspace", "create", "--cwd", cwd, "--label", label, focus]));
525
- const record = resource as Record<string, unknown>;
526
- const workspace = record.workspace as Record<string, unknown> | undefined;
527
- const tab = record.tab as Record<string, unknown> | undefined;
528
- const rootPane = record.root_pane as Record<string, unknown> | undefined;
529
- const workspaceId = typeof workspace?.workspace_id === "string" ? workspace.workspace_id : "";
530
- const tabId = typeof tab?.tab_id === "string" ? tab.tab_id : "";
531
- const paneId = typeof rootPane?.pane_id === "string" ? rootPane.pane_id : "";
532
- if (!workspaceId || !tabId || !paneId) throw new Error("Herdr workspace response did not include workspace, tab, and root pane identifiers");
533
- ownership.resources.set(workspaceId, { resourceId: workspaceId, level: "workspace", workspaceId, tabId, paneId, createdAt: new Date().toISOString() });
534
- ownership.panes.push({ workspaceId, tabId, paneId, resourceId: workspaceId });
535
- return { created: true, level: "workspace", resource };
536
- }
537
-
538
- if (operation === "tab") {
539
- const workspace = typeof params.workspaceId === "string" ? params.workspaceId : "";
540
- const cwd = requireAbsolutePath(params.cwd, "cwd");
541
- const label = typeof params.label === "string" && params.label.trim() ? params.label.trim() : `task-${randomUUID().slice(0, 8)}`;
542
- const focus = params.focus === true ? "--focus" : "--no-focus";
543
- const args = ["tab", "create", "--workspace", workspace, "--cwd", cwd, "--label", label, focus];
544
- if (!workspace) throw new Error("tab creation requires workspaceId");
545
- const resource = herdrResult(await runHerdr(args));
546
- const record = resource as Record<string, unknown>;
547
- const tab = record.tab as Record<string, unknown> | undefined;
548
- const rootPane = record.root_pane as Record<string, unknown> | undefined;
549
- const tabId = typeof tab?.tab_id === "string" ? tab.tab_id : "";
550
- const paneId = typeof rootPane?.pane_id === "string" ? rootPane.pane_id : "";
551
- if (!tabId || !paneId) throw new Error("Herdr tab response did not include tab and root pane identifiers");
552
- const resourceId = `${workspace}:${tabId}`;
553
- ownership.resources.set(resourceId, { resourceId, level: "tab", workspaceId: workspace, tabId, paneId, createdAt: new Date().toISOString() });
554
- ownership.panes.push({ workspaceId: workspace, tabId, paneId, resourceId });
555
- return { created: true, level: "tab", resource };
556
- }
557
-
558
- if (operation === "spawn") {
559
- const cwd = requireAbsolutePath(params.cwd, "cwd");
560
- const worktree = requireAbsolutePath(params.worktree, "worktree");
561
- if (!existsSync(worktree)) throw new Error(`worktree does not exist: ${worktree}`);
562
- if (worktree === cwd) throw new Error("worktree must be isolated from the pane cwd");
563
- const pane = process.env.HERDR_PANE_ID;
564
- if (!pane) throw new Error("HERDR_PANE_ID is missing; cannot spawn safely");
565
- const direction = params.direction === "down" ? "down" : "right";
566
- const beforeLayout = herdrResult(await runHerdr(["pane", "layout", "--pane", pane]));
567
- const beforePanes = new Set(collectValues(beforeLayout, "pane_id"));
568
- await runHerdr(["pane", "split", "--pane", pane, "--direction", direction, "--cwd", cwd, "--no-focus"]);
569
- const afterLayout = herdrResult(await runHerdr(["pane", "layout", "--pane", pane]));
570
- const newPane = collectValues(afterLayout, "pane_id").find((id) => !beforePanes.has(id));
571
- if (!newPane) throw new Error("Herdr split completed but no new pane id was returned");
572
- const paneRecords = collectPaneRecords(afterLayout);
573
- const layoutMetadata = afterLayout as Record<string, unknown>;
574
- const record = paneRecords.find((candidate) => candidate.paneId === newPane
575
- && candidate.workspaceId === String(layoutMetadata.workspace_id ?? candidate.workspaceId)
576
- && candidate.tabId === String(layoutMetadata.tab_id ?? candidate.tabId));
577
- if (!record) throw new Error("Herdr returned a pane without workspace/tab ownership metadata");
578
- const resourceId = `${record.workspaceId}:${record.tabId}:${record.paneId}`;
579
- ownership.resources.set(resourceId, { resourceId, level: "pane", workspaceId: record.workspaceId, tabId: record.tabId, paneId: record.paneId, createdAt: new Date().toISOString() });
580
- ownership.panes.push({ ...record, resourceId });
581
- await waitForInteractiveShell(newPane);
582
- return { pane: newPane, worktree, cwd, direction, ready: true, ownership: { ...record, resourceId } };
583
- }
584
-
585
- if (operation === "dispatch") {
586
- const name = params.name;
587
- if (typeof name !== "string" || !AGENT_NAME.test(name)) {
588
- throw new Error("name must match [a-z][a-z0-9_-]{0,31}");
589
- }
590
- const kind = typeof params.kind === "string" ? params.kind : "pi";
591
- const pane = typeof params.pane === "string" ? params.pane : "";
592
- const briefing = params.briefing;
593
- const worktree = requireAbsolutePath(params.worktree, "worktree");
594
- if (!existsSync(worktree)) throw new Error(`worktree does not exist: ${worktree}`);
595
- if (!pane || typeof briefing !== "string" || briefing.trim().length === 0) {
596
- throw new Error("dispatch requires pane and a non-empty briefing");
597
- }
598
- const ownedPane = ownership.panes.find((candidate) => candidate.paneId === pane);
599
- if (!ownedPane) throw new Error(`pane ${pane} is not owned by this Pi orchestration run`);
600
- const currentLayout = herdrResult(await runHerdr(["pane", "layout", "--pane", pane]));
601
- const currentRecord = collectPaneRecords(currentLayout).find((candidate) => candidate.paneId === pane);
602
- if (!currentRecord || currentRecord.workspaceId !== ownedPane.workspaceId || currentRecord.tabId !== ownedPane.tabId) {
603
- throw new Error(`pane ${pane} ownership metadata no longer matches this Pi orchestration run`);
604
- }
605
- await waitForInteractiveShell(pane);
606
- if (!briefing.includes(worktree)) {
607
- throw new Error("dispatch briefing must contain the absolute worktree path");
608
- }
609
- const start = await runHerdr(["agent", "start", name, "--kind", kind, "--pane", pane, "--timeout", "120000", "--"]);
610
- await runHerdr(["agent", "prompt", name, briefing]);
611
- // A fast agent can already be idle/done by the time it is inspected. The
612
- // server-owned wait is registered immediately so that completion is not
613
- // lost and a fast, successful turn is not falsely rejected.
614
- const execution: DelegationExecution = {
615
- executionId: randomUUID(),
616
- callerPaneId: process.env.HERDR_PANE_ID,
617
- workspaceId: ownedPane.workspaceId,
618
- tabId: ownedPane.tabId,
619
- paneId: ownedPane.paneId,
620
- resourceLevel: "pane",
621
- resourceId: ownedPane.resourceId,
622
- worktree,
623
- agentName: name,
624
- status: "working",
625
- runtimeState: "working",
626
- manualIntervention: false,
627
- createdAt: new Date().toISOString(),
628
- };
629
- ownership.executions.set(execution.executionId, execution);
630
- const wait = registerWait(name, 3_600_000, execution);
631
- let agentState: unknown;
632
- try {
633
- agentState = herdrResult(await runHerdr(["agent", "get", name]));
634
- } catch (error) {
635
- cancelWait(name);
636
- throw error;
637
- }
638
- if (!dispatchStateIsAcceptable(JSON.stringify(agentState))) {
639
- cancelWait(name);
640
- throw new Error(`Herdr agent ${name} entered an unsupported state after prompt submission`);
641
- }
642
- ownedPane.agentName = name;
643
- ownership.waits.add(name);
644
- return {
645
- started: herdrResult(start),
646
- state: agentState,
647
- wait,
648
- execution,
649
- ownership: ownedPane,
650
- };
651
- }
652
-
653
- if (operation === "status") {
654
- const executionId = params.executionId;
655
- if (typeof executionId !== "string") throw new Error("status requires executionId");
656
- const execution = ownership.executions.get(executionId);
657
- if (!execution) throw new Error(`execution ${executionId} is not owned by this Pi session`);
658
- if (execution.agentName) {
659
- const current = extractAgentLifecycleState(herdrResult(await runHerdr(["agent", "get", execution.agentName])));
660
- if (current) execution.runtimeState = current;
661
- }
662
- return execution;
663
- }
664
-
665
- if (operation === "mark_intervened") {
666
- const executionId = params.executionId;
667
- if (typeof executionId !== "string") throw new Error("mark_intervened requires executionId");
668
- const execution = ownership.executions.get(executionId);
669
- if (!execution) throw new Error(`execution ${executionId} is not owned by this Pi session`);
670
- execution.manualIntervention = true;
671
- execution.status = "intervened";
672
- return execution;
673
- }
674
-
675
- if (operation === "wait") {
676
- const name = params.name;
677
- if (typeof name !== "string" || !AGENT_NAME.test(name)) throw new Error("valid agent name is required");
678
- const timeout = typeof params.timeoutMs === "number" ? params.timeoutMs : 3_600_000;
679
- if (!ownership.waits.has(name)) throw new Error(`agent ${name} is not owned by this Pi orchestration run`);
680
- return registerWait(name, timeout);
681
- }
682
-
683
- if (operation === "cleanup") {
684
- const executionId = params.executionId;
685
- if (typeof executionId !== "string") throw new Error("cleanup requires executionId");
686
- const execution = ownership.executions.get(executionId);
687
- if (!execution) throw new Error(`execution ${executionId} is not owned by this Pi session`);
688
- return cleanupExecution(execution, ownership);
689
- }
690
-
691
- if (operation === "collect") {
692
- const storyId = params.storyId;
693
- if (typeof storyId !== "string" || storyId.length === 0) throw new Error("collect requires the story UUID as storyId");
694
- const story = await callAllye("allye_work_items", { action: "work_children", id: storyId });
695
- const reviewQuery = typeof params.storyKey === "string" ? `Review ${params.storyKey}` : `Review ${storyId}`;
696
- const review = await callAllye("allye_intelligence", { action: "memory_search", query: reviewQuery, limit: 10, return_content: true });
697
- const implementationQuery = typeof params.taskKey === "string" ? `Implementation ${params.taskKey}` : `Implementation ${storyId}`;
698
- const implementation = await callAllye("allye_intelligence", { action: "memory_search", query: implementationQuery, limit: 10, return_content: true });
699
- return { storyChildren: story, review, implementation, cleanupAvailable: typeof params.executionId === "string" };
700
- }
701
-
702
- throw new Error(`Unknown runtime operation: ${String(operation)}`);
703
- }
704
-
705
- function registerRuntimeTool(pi: ExtensionAPI, getCapabilities: () => AllyeCapabilities, registerWait: WaitRegistrar, cancelWait: WaitCanceller, state: DelegationState): void {
706
- pi.registerTool({
707
- name: RUNTIME_TOOL_NAME,
708
- label: "Allye Runtime",
709
- description: "Use optional Herdr capabilities for delegation, waiting, and collecting results when available.",
710
- parameters: Type.Object({
711
- operation: Type.String({ description: "detect | workspace | tab | spawn | dispatch | status | mark_intervened | wait | collect | cleanup" }),
712
- cwd: Type.Optional(Type.String({ description: "Absolute plugin-enabled repository root for spawn" })),
713
- worktree: Type.Optional(Type.String({ description: "Absolute isolated worktree path; required for spawn/dispatch" })),
714
- workspaceId: Type.Optional(Type.String({ description: "Workspace id for tab creation" })),
715
- label: Type.Optional(Type.String({ description: "Human-readable workspace/tab label" })),
716
- focus: Type.Optional(Type.Boolean({ description: "Focus newly created workspace/tab" })),
717
- direction: Type.Optional(Type.String({ description: "right or down" })),
718
- pane: Type.Optional(Type.String({ description: "Pane id returned by Herdr" })),
719
- name: Type.Optional(Type.String({ description: "Stable Herdr agent name" })),
720
- kind: Type.Optional(Type.String({ description: "Herdr agent kind, normally pi" })),
721
- briefing: Type.Optional(Type.String({ description: "Complete story briefing, including the absolute worktree path" })),
722
- timeoutMs: Type.Optional(Type.Number({ description: "Bounded wait timeout in milliseconds" })),
723
- executionId: Type.Optional(Type.String({ description: "Caller execution id for status/intervention/cleanup" })),
724
- storyId: Type.Optional(Type.String({ description: "Allye story UUID for collect" })),
725
- storyKey: Type.Optional(Type.String({ description: "Story key for review memory search" })),
726
- taskKey: Type.Optional(Type.String({ description: "Task key for implementation memory search" })),
727
- }),
728
- async execute(_toolCallId, params) {
729
- try {
730
- const result = await runtimeOperation(getCapabilities(), params as Record<string, unknown>, registerWait, cancelWait, state);
731
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], details: result };
732
- } catch (error) {
733
- const message = error instanceof Error ? error.message : String(error);
734
- return { content: [{ type: "text", text: `Allye runtime error: ${message}` }], isError: true, details: { error: message } };
735
- }
736
- },
737
- });
148
+ export function buildSystemSections(bootstrap: string, startup: StartupContext, firstPrompt: boolean, nativeBootstrap = true): string[] {
149
+ const sections: string[] = [];
150
+ if (bootstrap) sections.push(`<allye-bootstrap>\n${bootstrap.trim()}\n</allye-bootstrap>`);
151
+ if (startup.teamSelectionRequired) sections.push(`<allye-team-gate>\n${startup.text}\n</allye-team-gate>`);
152
+ else if (firstPrompt && nativeBootstrap && startup.text) sections.push(`<allye-startup-context>\n${startup.text}\n</allye-startup-context>`);
153
+ return sections;
738
154
  }
739
155
 
740
- export default function allyePiAdapter(pi: ExtensionAPI): void {
741
- const capabilities = describeCapabilities();
742
- let startupContext: StartupContext = { text: "", teamSelectionRequired: false, allyeUnavailable: true, teams: [] };
156
+ export default function allyePiExtension(pi: ExtensionAPI): void {
157
+ const nativeBootstrap = process.env.ALLYE_PI_NATIVE_BOOTSTRAP !== "0";
158
+ let startupContext = unavailableStartupContext("Allye context has not been loaded yet.");
159
+ let bootstrap = "";
743
160
  let firstPrompt = true;
744
- let usingAllyeBootstrap = "";
745
- let shuttingDown = false;
746
- let activeContext: ExtensionContext | undefined;
747
- const waits = new Map<string, AbortController>();
748
- const settledWaits = new Set<string>();
749
- const ownership: DelegationState = { panes: [], resources: new Map(), waits: new Set(), executions: new Map() };
750
-
751
- const registerWait: WaitRegistrar = (name, timeoutMs, execution) => {
752
- if (settledWaits.has(name)) return { registered: false, name, timeoutMs, duplicate: true, settled: true };
753
- const existing = waits.get(name);
754
- if (existing) return { registered: true, name, timeoutMs, duplicate: true };
755
- const controller = new AbortController();
756
- waits.set(name, controller);
757
- void pi.exec("herdr", ["agent", "wait", name, "--timeout", String(timeoutMs)], {
758
- timeout: timeoutMs + 5_000,
759
- signal: controller.signal,
760
- }).then((result) => {
761
- const event: WaitEvent = {
762
- kind: execution?.manualIntervention ? "intervened" : "settled",
763
- executionId: execution?.executionId,
764
- name,
765
- timeoutMs,
766
- outcome: classifyWaitResult(result, timeoutMs),
767
- code: result.code,
768
- killed: result.killed,
769
- stdout: result.stdout,
770
- stderr: result.stderr,
771
- timestamp: new Date().toISOString(),
772
- };
773
- if (execution) {
774
- execution.status = execution.manualIntervention ? "intervened" : event.outcome === "blocked" ? "blocked" : event.outcome === "completed" ? "settled" : "unknown";
775
- execution.runtimeState = event.outcome === "completed" ? "done" : event.outcome === "blocked" ? "blocked" : "unknown";
776
- execution.settledAt = event.timestamp;
777
- }
778
- deliverWaitEvent(event, shuttingDown, settledWaits, {
779
- appendEntry: (customType, value) => pi.appendEntry(customType, value),
780
- notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
781
- sendMessage: (message, options) => pi.sendMessage(message, options),
782
- });
783
- }).catch((error) => {
784
- const event: WaitEvent = {
785
- kind: execution?.manualIntervention ? "intervened" : "settled",
786
- executionId: execution?.executionId,
787
- name,
788
- timeoutMs,
789
- outcome: classifyWaitResult(undefined, timeoutMs, error),
790
- error: error instanceof Error ? error.message : String(error),
791
- timestamp: new Date().toISOString(),
792
- };
793
- if (execution) {
794
- execution.status = execution.manualIntervention ? "intervened" : event.outcome === "blocked" ? "blocked" : event.outcome === "completed" ? "settled" : "unknown";
795
- execution.runtimeState = event.outcome === "completed" ? "done" : event.outcome === "blocked" ? "blocked" : "unknown";
796
- execution.settledAt = event.timestamp;
797
- }
798
- deliverWaitEvent(event, shuttingDown, settledWaits, {
799
- appendEntry: (customType, value) => pi.appendEntry(customType, value),
800
- notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
801
- sendMessage: (message, options) => pi.sendMessage(message, options),
802
- });
803
- }).finally(() => {
804
- waits.delete(name);
805
- });
806
- return {
807
- registered: true,
808
- name,
809
- timeoutMs,
810
- diagnostic: "Wait runs through Pi's managed exec, is bounded, and records stdout/stderr in the session log.",
811
- };
812
- };
813
161
 
814
- const cancelWait: WaitCanceller = (name) => {
815
- waits.get(name)?.abort();
816
- waits.delete(name);
817
- };
818
-
819
- const syncCapabilityTools = () => {
820
- pi.setActiveTools(activeToolsForCapabilities(pi.getActiveTools(), capabilities));
821
- };
822
-
823
- pi.registerMessageRenderer(WAIT_EVENT_TYPE, () => undefined);
824
-
825
- pi.on("resources_discover", () => ({ skillPaths: [canonicalSkillsPath()] }));
162
+ pi.on("resources_discover", () => ({ skillPaths: [skillsPath()] }));
826
163
 
827
164
  pi.on("session_start", async (_event, ctx) => {
828
- shuttingDown = false;
829
- activeContext = ctx;
830
165
  firstPrompt = true;
831
- usingAllyeBootstrap = loadUsingAllyeSkill();
832
- syncCapabilityTools();
833
- if (ctx.hasUI) ctx.ui.setStatus("allye-pi", "loading Allye context…");
834
- if (process.env.ALLYE_PI_NATIVE_BOOTSTRAP !== "0") startupContext = await loadStartupContext(capabilities.allyeMcp);
166
+ bootstrap = loadBootstrap();
167
+ if (nativeBootstrap) {
168
+ if (ctx.hasUI) ctx.ui.setStatus("allye", "loading Allye context…");
169
+ startupContext = await loadStartupContext();
170
+ if (ctx.hasUI) ctx.ui.setStatus("allye", startupContext.allyeUnavailable ? "Allye: context not preloaded" : "Allye ready");
171
+ }
835
172
  if (startupContext.teamSelectionRequired && ctx.hasUI) {
836
- ctx.ui.notify("Allye has multiple teams and no active team. Use /allye-team <name|prefix|id> before team-scoped work.", "warning");
173
+ ctx.ui.notify("Allye has several teams and none is active. Use /allye-team <name|prefix|id> before team-scoped work.", "warning");
837
174
  }
838
- if (ctx.hasUI) ctx.ui.setStatus("allye-pi", "Allye ready");
839
- });
840
-
841
- pi.on("session_shutdown", () => {
842
- shuttingDown = true;
843
- activeContext = undefined;
844
- for (const controller of waits.values()) controller.abort();
845
- waits.clear();
846
- ownership.waits.clear();
847
- // Owned Herdr resources intentionally remain open across a Pi shutdown.
848
- // Cleanup requires an explicit, verified cleanup operation and never runs
849
- // implicitly while the caller cannot inspect its final state.
850
175
  });
851
176
 
852
177
  pi.on("before_agent_start", async (event) => {
853
- const sections = [
854
- `<allye-pi-adapter>\n${toolkitInstructions()}\n\nCanonical Allye skills are loaded from ${canonicalSkillsPath()} — do not create copies.\n${startupContext.teamSelectionRequired ? "\n" + startupContext.text : ""}\n</allye-pi-adapter>`,
855
- ];
178
+ const sections = buildSystemSections(bootstrap, startupContext, firstPrompt, nativeBootstrap);
856
179
  if (firstPrompt) {
857
180
  firstPrompt = false;
858
- if (usingAllyeBootstrap) sections.push(`<allye-using-allye-bootstrap>\n${usingAllyeBootstrap}\n</allye-using-allye-bootstrap>`);
859
- if (process.env.ALLYE_PI_NATIVE_BOOTSTRAP !== "0" && startupContext.text) sections.push(`<allye-pi-startup-context>\n${startupContext.text}\n</allye-pi-startup-context>`);
860
- if (!startupContext.teamSelectionRequired && !startupContext.allyeUnavailable) {
181
+ if (nativeBootstrap && !startupContext.teamSelectionRequired && !startupContext.allyeUnavailable) {
861
182
  const relevant = await loadPromptContext(event.prompt);
862
- if (relevant) sections.push(`<allye-pi-relevant-memory>\n${relevant}\n</allye-pi-relevant-memory>`);
183
+ if (relevant) sections.push(`<allye-relevant-memory>\n${relevant}\n</allye-relevant-memory>`);
863
184
  }
864
185
  }
865
- return { systemPrompt: `${event.systemPrompt}\n\n${sections.join("\n\n")}` };
866
- });
867
-
868
- pi.registerCommand("allye-capabilities", {
869
- description: "Show capabilities available to the Allye toolkit in this Pi session",
870
- handler: async (_args, ctx) => {
871
- const available = [
872
- `Allye/MCP: ${capabilities.allyeMcp ? "available" : "unavailable"}`,
873
- "filesystem: available",
874
- `subagents: ${capabilities.subagents ? "available" : "unavailable"}`,
875
- `Herdr: ${capabilities.herdr ? "available" : "unavailable"}`,
876
- ].join("; ");
877
- ctx.ui.notify(`${available}. Tasks are recommended, not mandatory.`, "info");
878
- },
186
+ return sections.length ? { systemPrompt: `${event.systemPrompt}\n\n${sections.join("\n\n")}` } : undefined;
879
187
  });
880
188
 
881
189
  pi.registerCommand("allye-team", {
882
- description: "Select the active Allye team without choosing one silently",
190
+ description: "Select the active Allye team (never chosen silently)",
883
191
  handler: async (args, ctx) => {
884
192
  const teamQuery = args?.trim();
885
193
  if (!teamQuery) {
@@ -887,14 +195,12 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
887
195
  return;
888
196
  }
889
197
  try {
890
- const result = await callAllye("allye_team", { action: "team_switch", team_query: teamQuery });
198
+ const result = await callAllye("team", { action: "team_switch", team_query: teamQuery });
891
199
  startupContext = await loadStartupContext();
892
200
  ctx.ui.notify(`Allye team selection: ${result}`, "info");
893
201
  } catch (error) {
894
- ctx.ui.notify(`Allye team selection failed: ${error instanceof Error ? error.message : String(error)}`, "error");
202
+ ctx.ui.notify(`Allye team selection failed: ${error instanceof Error ? error.message : String(error)}. Ask the agent to call the \`team\` tool with action team_switch instead.`, "error");
895
203
  }
896
204
  },
897
205
  });
898
-
899
- registerRuntimeTool(pi, () => capabilities, registerWait, cancelWait, ownership);
900
206
  }