allye-pi 1.8.0 → 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.
- package/README.md +75 -271
- package/bootstrap/allye.md +30 -0
- package/docs/install-omp.md +31 -0
- package/docs/install-pi.md +21 -125
- package/package.json +10 -5
- package/packages/allye-pi/src/index.ts +88 -782
- package/skills/bridge/SKILL.md +105 -0
- package/skills/bridge/references/crew.md +20 -0
- package/skills/bridge/references/delegation.md +174 -0
- package/skills/bridge/references/discovery.md +71 -0
- package/skills/bridge/references/publish.md +90 -0
- package/skills/bridge/references/state-contract.md +109 -0
- package/skills/bridge/references/workflow.md +132 -0
- package/skills/bridge-architect/SKILL.md +96 -0
- package/skills/bridge-architect/references/spec-template.md +90 -0
- package/skills/bridge-armorer/SKILL.md +103 -0
- package/skills/bridge-copilot/SKILL.md +74 -0
- package/skills/bridge-dispatcher/SKILL.md +96 -0
- package/skills/bridge-dispatcher/references/spec-template-minimal.md +53 -0
- package/skills/bridge-medic/SKILL.md +81 -0
- package/skills/bridge-optimizer/SKILL.md +68 -0
- package/skills/bridge-pilot/SKILL.md +69 -0
- package/skills/bridge-pilot/references/mocking.md +57 -0
- package/skills/bridge-pilot/references/tdd.md +42 -0
- package/skills/bridge-pilot/references/tests.md +60 -0
- package/skills/bridge-recon/SKILL.md +82 -0
- package/skills/bridge-shield/SKILL.md +73 -0
- package/skills/bridge-strategist/SKILL.md +92 -0
- package/skills/bridge-watcher/SKILL.md +73 -0
- package/skills/agent-runtime/SKILL.md +0 -129
- package/skills/agent-runtime/references/herdr.md +0 -184
- package/skills/board-progression/SKILL.md +0 -214
- package/skills/branch-landing/SKILL.md +0 -88
- package/skills/delivery/SKILL.md +0 -187
- package/skills/execution/SKILL.md +0 -396
- package/skills/handover-protocol/SKILL.md +0 -74
- package/skills/handover-protocol/references/correction.md +0 -32
- package/skills/handover-protocol/references/discovery-to-planning.md +0 -44
- package/skills/handover-protocol/references/execution-report.md +0 -42
- package/skills/handover-protocol/references/planning-to-technical.md +0 -42
- package/skills/handover-protocol/references/story-execution.md +0 -74
- package/skills/handover-protocol/references/technical-to-orchestration.md +0 -45
- package/skills/memory-protocol/SKILL.md +0 -319
- package/skills/orchestrator/SKILL.md +0 -249
- package/skills/product-planning/SKILL.md +0 -257
- package/skills/review/SKILL.md +0 -165
- package/skills/sandbox/SKILL.md +0 -62
- package/skills/setup/SKILL.md +0 -226
- package/skills/tdd-workflow/SKILL.md +0 -144
- package/skills/technical-planning/SKILL.md +0 -354
- package/skills/tools-quickref/SKILL.md +0 -307
- package/skills/using-allye/SKILL.md +0 -171
- 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 {
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
5
3
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
138
|
-
|
|
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
|
|
41
|
+
export function loadBootstrap(): string {
|
|
148
42
|
try {
|
|
149
|
-
return readFileSync(resolve(
|
|
43
|
+
return readFileSync(resolve(packageRoot, "bootstrap", "allye.md"), "utf8");
|
|
150
44
|
} catch {
|
|
151
45
|
return "";
|
|
152
46
|
}
|
|
153
47
|
}
|
|
154
48
|
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
98
|
+
return unavailableStartupContext("Allye `initialize` returned a payload this extension could not interpret.");
|
|
198
99
|
}
|
|
199
|
-
const
|
|
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
|
|
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
|
|
211
|
-
? `## Allye team selection required\nThis account has
|
|
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
|
|
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
|
-
|
|
307
|
-
|
|
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("
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
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("
|
|
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
|
|
358
|
-
|
|
359
|
-
${
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
|
741
|
-
const
|
|
742
|
-
let startupContext
|
|
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
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
|
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 (
|
|
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-
|
|
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
|
|
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("
|
|
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)}
|
|
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
|
}
|