allye-pi 1.6.1 → 1.7.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/docs/install-pi.md
CHANGED
|
@@ -81,8 +81,9 @@ adaptive toolkit in the current session. Use `/allye-capabilities` to inspect
|
|
|
81
81
|
Allye/MCP, filesystem, subagent, and Herdr capabilities.
|
|
82
82
|
|
|
83
83
|
When `HERDR_ENV=1`, the optional `allye_herdr` tool provides bounded `detect`,
|
|
84
|
-
`spawn`, `dispatch`, `
|
|
85
|
-
|
|
84
|
+
`workspace`, `tab`, `spawn`, `dispatch`, `status`, `mark_intervened`, `wait`,
|
|
85
|
+
`collect`, and ownership-guarded `cleanup` operations. Without it, Pi continues
|
|
86
|
+
locally. Herdr and subagents are capabilities, not prerequisites.
|
|
86
87
|
Tasks are recommended for meaningful, delegated, multi-step, or review-heavy
|
|
87
88
|
work, but an explicitly approved no-task path is supported.
|
|
88
89
|
|
|
@@ -138,6 +139,8 @@ Manual `herdr agent wait` commands started outside this Pi session (for example
|
|
|
138
139
|
with `nohup` or Bash) do not wake Pi and do not generate this notification.
|
|
139
140
|
|
|
140
141
|
A real Herdr dispatch still requires `HERDR_ENV=1`, a compatible Herdr session,
|
|
141
|
-
and an existing isolated worktree. Pi records
|
|
142
|
-
for safe teardown; it never adopts foreign
|
|
143
|
-
merges, pushes, or publishes without explicit
|
|
142
|
+
and an existing isolated worktree. Pi records execution IDs plus
|
|
143
|
+
workspace/tab/pane/agent ownership for safe teardown; it never adopts foreign
|
|
144
|
+
panes, stops Herdr, closes panes, merges, pushes, or publishes without explicit
|
|
145
|
+
authorization. Cleanup is only allowed after a settled and collected execution;
|
|
146
|
+
blocked, unknown, timed-out, interrupted, or manually assumed resources remain open.
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { promisify } from "node:util";
|
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { Type } from "typebox";
|
|
7
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
|
|
@@ -26,9 +27,9 @@ type StartupContext = {
|
|
|
26
27
|
allyeUnavailable: boolean;
|
|
27
28
|
teams: Array<{ id: string; name: string; prefix?: string }>;
|
|
28
29
|
};
|
|
29
|
-
type WaitRegistrar = (name: string, timeoutMs: number) => Record<string, unknown>;
|
|
30
|
+
type WaitRegistrar = (name: string, timeoutMs: number, execution?: DelegationExecution) => Record<string, unknown>;
|
|
30
31
|
type WaitCanceller = (name: string) => void;
|
|
31
|
-
export type WaitOutcome = "completed" | "timeout" | "error" | "aborted";
|
|
32
|
+
export type WaitOutcome = "completed" | "blocked" | "unknown" | "timeout" | "error" | "aborted";
|
|
32
33
|
export type WaitExecutionResult = {
|
|
33
34
|
code: number;
|
|
34
35
|
killed: boolean;
|
|
@@ -36,6 +37,8 @@ export type WaitExecutionResult = {
|
|
|
36
37
|
stderr: string;
|
|
37
38
|
};
|
|
38
39
|
export type WaitEvent = {
|
|
40
|
+
kind?: "settled" | "intervened";
|
|
41
|
+
executionId?: string;
|
|
39
42
|
name: string;
|
|
40
43
|
timeoutMs: number;
|
|
41
44
|
outcome: WaitOutcome;
|
|
@@ -66,10 +69,37 @@ export type OwnedPane = {
|
|
|
66
69
|
tabId: string;
|
|
67
70
|
paneId: string;
|
|
68
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;
|
|
69
97
|
};
|
|
70
98
|
export type DelegationState = {
|
|
71
99
|
panes: OwnedPane[];
|
|
100
|
+
resources: Map<string, OwnedResource>;
|
|
72
101
|
waits: Set<string>;
|
|
102
|
+
executions: Map<string, DelegationExecution>;
|
|
73
103
|
};
|
|
74
104
|
const WAIT_EVENT_TYPE = "allye-herdr-wait";
|
|
75
105
|
const RUNTIME_TOOL_NAME = "allye_herdr";
|
|
@@ -87,10 +117,17 @@ function limitText(text: string, maxChars: number): string {
|
|
|
87
117
|
return text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n[context truncated]`;
|
|
88
118
|
}
|
|
89
119
|
|
|
90
|
-
|
|
120
|
+
type McpBridgeRuntime = typeof globalThis & {
|
|
121
|
+
__piMcpAdapterActiveToolCaller?: ActiveMcpToolCaller | null;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export function describeCapabilities(
|
|
125
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
126
|
+
mcpAvailable = Boolean((globalThis as McpBridgeRuntime).__piMcpAdapterActiveToolCaller),
|
|
127
|
+
): AllyeCapabilities {
|
|
91
128
|
return {
|
|
92
129
|
piSession: true,
|
|
93
|
-
allyeMcp: env.ALLYE_PI_MCP !== "0",
|
|
130
|
+
allyeMcp: env.ALLYE_PI_MCP !== "0" && mcpAvailable,
|
|
94
131
|
filesystem: true,
|
|
95
132
|
subagents: env.ALLYE_PI_SUBAGENTS === "1",
|
|
96
133
|
herdr: env.HERDR_ENV === "1",
|
|
@@ -133,9 +170,7 @@ async function configuredMcpCall(toolName: string, args: Record<string, unknown>
|
|
|
133
170
|
// the in-process runtime. There is no public callMcpTool export in the
|
|
134
171
|
// supported package version, so do not dynamically import its TypeScript
|
|
135
172
|
// source from node_modules. A missing bridge is a hard bootstrap failure.
|
|
136
|
-
const globalBridge = (globalThis as
|
|
137
|
-
__piMcpAdapterActiveToolCaller?: ActiveMcpToolCaller | null;
|
|
138
|
-
}).__piMcpAdapterActiveToolCaller;
|
|
173
|
+
const globalBridge = (globalThis as McpBridgeRuntime).__piMcpAdapterActiveToolCaller;
|
|
139
174
|
if (!globalBridge) throw new Error("pi-mcp-adapter is not initialized; start the configured MCP adapter before using Allye");
|
|
140
175
|
return globalBridge(MCP_SERVER, toolName, args);
|
|
141
176
|
}
|
|
@@ -182,6 +217,11 @@ export function classifyWaitResult(result?: WaitExecutionResult, timeoutMs = 0,
|
|
|
182
217
|
const errorText = error instanceof Error ? error.message : String(error ?? "");
|
|
183
218
|
if (/abort/i.test(errorText)) return "aborted";
|
|
184
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";
|
|
185
225
|
if (!result || result.code !== 0) return "error";
|
|
186
226
|
return "completed";
|
|
187
227
|
}
|
|
@@ -195,7 +235,8 @@ export function shouldEmitWaitEvent(shuttingDown: boolean, settledWaits: Set<str
|
|
|
195
235
|
export function formatWaitEvent(event: WaitEvent): string {
|
|
196
236
|
const output = event.stdout?.trim() || "(no stdout)";
|
|
197
237
|
const error = event.error || event.stderr?.trim() || "(none)";
|
|
198
|
-
|
|
238
|
+
const prefix = event.kind === "intervened" ? "Herdr agent intervention detected" : "Herdr wait settled";
|
|
239
|
+
return `${prefix} for agent ${event.name}. Outcome: ${event.outcome}. `
|
|
199
240
|
+ `This is delegation evidence only, not a completion verdict. `
|
|
200
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`
|
|
201
242
|
+ `timeout_ms=${event.timeoutMs} code=${event.code ?? "n/a"} killed=${event.killed ?? false} timestamp=${event.timestamp}\n`
|
|
@@ -203,7 +244,7 @@ export function formatWaitEvent(event: WaitEvent): string {
|
|
|
203
244
|
}
|
|
204
245
|
|
|
205
246
|
export function waitEventNotificationLevel(event: WaitEvent): "info" | "warning" | "error" {
|
|
206
|
-
if (event.outcome === "error") return "error";
|
|
247
|
+
if (event.outcome === "error" || event.outcome === "blocked" || event.outcome === "unknown") return "error";
|
|
207
248
|
if (event.outcome === "timeout") return "warning";
|
|
208
249
|
return "info";
|
|
209
250
|
}
|
|
@@ -255,8 +296,8 @@ export function deliverWaitEvent(
|
|
|
255
296
|
|
|
256
297
|
export function invalidStartupContext(message: string): StartupContext {
|
|
257
298
|
return {
|
|
258
|
-
text: `## Allye
|
|
259
|
-
teamSelectionRequired:
|
|
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,
|
|
260
301
|
allyeUnavailable: true,
|
|
261
302
|
teams: [],
|
|
262
303
|
};
|
|
@@ -266,7 +307,8 @@ export function canUseOwnedPane(state: DelegationState, paneId: string): boolean
|
|
|
266
307
|
return state.panes.some((pane) => pane.paneId === paneId);
|
|
267
308
|
}
|
|
268
309
|
|
|
269
|
-
async function loadStartupContext(): Promise<StartupContext> {
|
|
310
|
+
async function loadStartupContext(allyeMcp = true): Promise<StartupContext> {
|
|
311
|
+
if (!allyeMcp) return invalidStartupContext("Allye/MCP is disabled for this Pi session.");
|
|
270
312
|
const sections: string[] = [];
|
|
271
313
|
try {
|
|
272
314
|
const initialization = await callAllye("allye_initialize", {
|
|
@@ -406,9 +448,30 @@ export function activeToolsForCapabilities(activeTools: string[], capabilities:
|
|
|
406
448
|
return [...next];
|
|
407
449
|
}
|
|
408
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
|
+
|
|
409
472
|
export function dispatchStateIsAcceptable(stateText: string): boolean {
|
|
410
|
-
|
|
411
|
-
return
|
|
473
|
+
const state = extractAgentLifecycleState(parseJson(stateText));
|
|
474
|
+
return state === "working" || state === "idle" || state === "done";
|
|
412
475
|
}
|
|
413
476
|
|
|
414
477
|
function requireHerdr(capabilities: AllyeCapabilities): void {
|
|
@@ -422,6 +485,23 @@ function requireAbsolutePath(value: unknown, name: string): string {
|
|
|
422
485
|
return value;
|
|
423
486
|
}
|
|
424
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
|
+
|
|
425
505
|
async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<string, unknown>, registerWait: WaitRegistrar, cancelWait: WaitCanceller, ownership: DelegationState): Promise<unknown> {
|
|
426
506
|
const operation = params.operation;
|
|
427
507
|
if (operation === "detect") {
|
|
@@ -436,6 +516,45 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
436
516
|
|
|
437
517
|
requireHerdr(capabilities);
|
|
438
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
|
+
|
|
439
558
|
if (operation === "spawn") {
|
|
440
559
|
const cwd = requireAbsolutePath(params.cwd, "cwd");
|
|
441
560
|
const worktree = requireAbsolutePath(params.worktree, "worktree");
|
|
@@ -456,9 +575,11 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
456
575
|
&& candidate.workspaceId === String(layoutMetadata.workspace_id ?? candidate.workspaceId)
|
|
457
576
|
&& candidate.tabId === String(layoutMetadata.tab_id ?? candidate.tabId));
|
|
458
577
|
if (!record) throw new Error("Herdr returned a pane without workspace/tab ownership metadata");
|
|
459
|
-
|
|
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 });
|
|
460
581
|
await waitForInteractiveShell(newPane);
|
|
461
|
-
return { pane: newPane, worktree, cwd, direction, ready: true, ownership: record };
|
|
582
|
+
return { pane: newPane, worktree, cwd, direction, ready: true, ownership: { ...record, resourceId } };
|
|
462
583
|
}
|
|
463
584
|
|
|
464
585
|
if (operation === "dispatch") {
|
|
@@ -490,7 +611,23 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
490
611
|
// A fast agent can already be idle/done by the time it is inspected. The
|
|
491
612
|
// server-owned wait is registered immediately so that completion is not
|
|
492
613
|
// lost and a fast, successful turn is not falsely rejected.
|
|
493
|
-
const
|
|
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);
|
|
494
631
|
let agentState: unknown;
|
|
495
632
|
try {
|
|
496
633
|
agentState = herdrResult(await runHerdr(["agent", "get", name]));
|
|
@@ -508,10 +645,33 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
508
645
|
started: herdrResult(start),
|
|
509
646
|
state: agentState,
|
|
510
647
|
wait,
|
|
648
|
+
execution,
|
|
511
649
|
ownership: ownedPane,
|
|
512
650
|
};
|
|
513
651
|
}
|
|
514
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
|
+
|
|
515
675
|
if (operation === "wait") {
|
|
516
676
|
const name = params.name;
|
|
517
677
|
if (typeof name !== "string" || !AGENT_NAME.test(name)) throw new Error("valid agent name is required");
|
|
@@ -520,6 +680,14 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
520
680
|
return registerWait(name, timeout);
|
|
521
681
|
}
|
|
522
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
|
+
|
|
523
691
|
if (operation === "collect") {
|
|
524
692
|
const storyId = params.storyId;
|
|
525
693
|
if (typeof storyId !== "string" || storyId.length === 0) throw new Error("collect requires the story UUID as storyId");
|
|
@@ -528,7 +696,7 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
528
696
|
const review = await callAllye("allye_intelligence", { action: "memory_search", query: reviewQuery, limit: 10, return_content: true });
|
|
529
697
|
const implementationQuery = typeof params.taskKey === "string" ? `Implementation ${params.taskKey}` : `Implementation ${storyId}`;
|
|
530
698
|
const implementation = await callAllye("allye_intelligence", { action: "memory_search", query: implementationQuery, limit: 10, return_content: true });
|
|
531
|
-
return { storyChildren: story, review, implementation };
|
|
699
|
+
return { storyChildren: story, review, implementation, cleanupAvailable: typeof params.executionId === "string" };
|
|
532
700
|
}
|
|
533
701
|
|
|
534
702
|
throw new Error(`Unknown runtime operation: ${String(operation)}`);
|
|
@@ -540,15 +708,19 @@ function registerRuntimeTool(pi: ExtensionAPI, getCapabilities: () => AllyeCapab
|
|
|
540
708
|
label: "Allye Runtime",
|
|
541
709
|
description: "Use optional Herdr capabilities for delegation, waiting, and collecting results when available.",
|
|
542
710
|
parameters: Type.Object({
|
|
543
|
-
operation: Type.String({ description: "detect | spawn | dispatch | wait | collect" }),
|
|
711
|
+
operation: Type.String({ description: "detect | workspace | tab | spawn | dispatch | status | mark_intervened | wait | collect | cleanup" }),
|
|
544
712
|
cwd: Type.Optional(Type.String({ description: "Absolute plugin-enabled repository root for spawn" })),
|
|
545
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" })),
|
|
546
717
|
direction: Type.Optional(Type.String({ description: "right or down" })),
|
|
547
718
|
pane: Type.Optional(Type.String({ description: "Pane id returned by Herdr" })),
|
|
548
719
|
name: Type.Optional(Type.String({ description: "Stable Herdr agent name" })),
|
|
549
720
|
kind: Type.Optional(Type.String({ description: "Herdr agent kind, normally pi" })),
|
|
550
721
|
briefing: Type.Optional(Type.String({ description: "Complete story briefing, including the absolute worktree path" })),
|
|
551
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" })),
|
|
552
724
|
storyId: Type.Optional(Type.String({ description: "Allye story UUID for collect" })),
|
|
553
725
|
storyKey: Type.Optional(Type.String({ description: "Story key for review memory search" })),
|
|
554
726
|
taskKey: Type.Optional(Type.String({ description: "Task key for implementation memory search" })),
|
|
@@ -574,9 +746,9 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
574
746
|
let activeContext: ExtensionContext | undefined;
|
|
575
747
|
const waits = new Map<string, AbortController>();
|
|
576
748
|
const settledWaits = new Set<string>();
|
|
577
|
-
const ownership: DelegationState = { panes: [], waits: new Set() };
|
|
749
|
+
const ownership: DelegationState = { panes: [], resources: new Map(), waits: new Set(), executions: new Map() };
|
|
578
750
|
|
|
579
|
-
const registerWait: WaitRegistrar = (name, timeoutMs) => {
|
|
751
|
+
const registerWait: WaitRegistrar = (name, timeoutMs, execution) => {
|
|
580
752
|
if (settledWaits.has(name)) return { registered: false, name, timeoutMs, duplicate: true, settled: true };
|
|
581
753
|
const existing = waits.get(name);
|
|
582
754
|
if (existing) return { registered: true, name, timeoutMs, duplicate: true };
|
|
@@ -587,6 +759,8 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
587
759
|
signal: controller.signal,
|
|
588
760
|
}).then((result) => {
|
|
589
761
|
const event: WaitEvent = {
|
|
762
|
+
kind: execution?.manualIntervention ? "intervened" : "settled",
|
|
763
|
+
executionId: execution?.executionId,
|
|
590
764
|
name,
|
|
591
765
|
timeoutMs,
|
|
592
766
|
outcome: classifyWaitResult(result, timeoutMs),
|
|
@@ -596,6 +770,11 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
596
770
|
stderr: result.stderr,
|
|
597
771
|
timestamp: new Date().toISOString(),
|
|
598
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
|
+
}
|
|
599
778
|
deliverWaitEvent(event, shuttingDown, settledWaits, {
|
|
600
779
|
appendEntry: (customType, value) => pi.appendEntry(customType, value),
|
|
601
780
|
notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
|
|
@@ -603,12 +782,19 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
603
782
|
});
|
|
604
783
|
}).catch((error) => {
|
|
605
784
|
const event: WaitEvent = {
|
|
785
|
+
kind: execution?.manualIntervention ? "intervened" : "settled",
|
|
786
|
+
executionId: execution?.executionId,
|
|
606
787
|
name,
|
|
607
788
|
timeoutMs,
|
|
608
789
|
outcome: classifyWaitResult(undefined, timeoutMs, error),
|
|
609
790
|
error: error instanceof Error ? error.message : String(error),
|
|
610
791
|
timestamp: new Date().toISOString(),
|
|
611
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
|
+
}
|
|
612
798
|
deliverWaitEvent(event, shuttingDown, settledWaits, {
|
|
613
799
|
appendEntry: (customType, value) => pi.appendEntry(customType, value),
|
|
614
800
|
notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
|
|
@@ -645,7 +831,7 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
645
831
|
usingAllyeBootstrap = loadUsingAllyeSkill();
|
|
646
832
|
syncCapabilityTools();
|
|
647
833
|
if (ctx.hasUI) ctx.ui.setStatus("allye-pi", "loading Allye context…");
|
|
648
|
-
if (process.env.ALLYE_PI_NATIVE_BOOTSTRAP !== "0") startupContext = await loadStartupContext();
|
|
834
|
+
if (process.env.ALLYE_PI_NATIVE_BOOTSTRAP !== "0") startupContext = await loadStartupContext(capabilities.allyeMcp);
|
|
649
835
|
if (startupContext.teamSelectionRequired && ctx.hasUI) {
|
|
650
836
|
ctx.ui.notify("Allye has multiple teams and no active team. Use /allye-team <name|prefix|id> before team-scoped work.", "warning");
|
|
651
837
|
}
|
|
@@ -658,7 +844,9 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
658
844
|
for (const controller of waits.values()) controller.abort();
|
|
659
845
|
waits.clear();
|
|
660
846
|
ownership.waits.clear();
|
|
661
|
-
|
|
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.
|
|
662
850
|
});
|
|
663
851
|
|
|
664
852
|
pi.on("before_agent_start", async (event) => {
|
|
@@ -13,7 +13,13 @@ any runtime that satisfies the contract rather than one specific tool.
|
|
|
13
13
|
|
|
14
14
|
Load this only when a compatible runtime is actually detected and the user benefits from delegation. Without one, continue locally or use another available subagent mechanism; never let the absence or degradation of a runtime block delivery.
|
|
15
15
|
|
|
16
|
-
Concrete commands for the runtimes we implement live in `references/`.
|
|
16
|
+
Concrete commands for the runtimes we implement live in `references/`.
|
|
17
|
+
|
|
18
|
+
For Herdr, the Pi adapter exposes `workspace` and `tab` creation in addition to
|
|
19
|
+
`spawn`, and returns an execution ID from `dispatch`. The caller must retain this
|
|
20
|
+
ID, use `status`/`wait`, collect the result, and only then request ownership-guarded
|
|
21
|
+
`cleanup`. `mark_intervened` records human takeover and permanently blocks automatic
|
|
22
|
+
cleanup for that execution. This file is the
|
|
17
23
|
contract; the reference is the implementation.
|
|
18
24
|
|
|
19
25
|
<!-- adapted from ogulcancelik/herdr SKILL.md (Apache-2.0) — the primitive vocabulary and the safety rules -->
|
|
@@ -22,6 +22,18 @@ fails in a way that suggests the server went away.
|
|
|
22
22
|
Caller context is injected into every managed pane: `$HERDR_PANE_ID`, `$HERDR_TAB_ID`,
|
|
23
23
|
`$HERDR_WORKSPACE_ID`.
|
|
24
24
|
|
|
25
|
+
## workspace and tab ownership
|
|
26
|
+
|
|
27
|
+
The Pi adapter can create a workspace or a tab without focusing it:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
allye_herdr(operation="workspace", cwd="/absolute/repo", label="feature-runtime")
|
|
31
|
+
allye_herdr(operation="tab", workspaceId="w...", cwd="/absolute/repo", label="story-1")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The returned IDs are registered as owned resources. Never infer IDs from layout
|
|
35
|
+
order and never use these operations to adopt an existing user resource.
|
|
36
|
+
|
|
25
37
|
## spawn
|
|
26
38
|
|
|
27
39
|
```bash
|
|
@@ -132,13 +144,31 @@ herdr agent send-keys {name} esc
|
|
|
132
144
|
Then dispatch normally, including the submit confirmation — a freshly dismissed menu leaves
|
|
133
145
|
the agent idle, which is exactly the state where a prompt lands unsubmitted.
|
|
134
146
|
|
|
135
|
-
## teardown
|
|
147
|
+
## execution status, intervention, and teardown
|
|
148
|
+
|
|
149
|
+
Each dispatch returns an `executionId`. Retain it for the whole lifecycle:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
allye_herdr(operation="status", executionId="...")
|
|
153
|
+
allye_herdr(operation="wait", name="story-agent", timeoutMs=3600000)
|
|
154
|
+
allye_herdr(operation="mark_intervened", executionId="...")
|
|
155
|
+
allye_herdr(operation="cleanup", executionId="...")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use `mark_intervened` when the user takes over or modifies the managed pane. A
|
|
159
|
+
manual intervention, `blocked`, `unknown`, timeout, or missing collection must
|
|
160
|
+
keep the resource open. Cleanup is valid only after wait settlement, Allye/result
|
|
161
|
+
collection, and verification.
|
|
162
|
+
|
|
163
|
+
The adapter rejects cleanup for resources it did not create and for executions
|
|
164
|
+
that are still working, blocked, unknown, or manually intervened.
|
|
136
165
|
|
|
137
166
|
```bash
|
|
138
167
|
herdr pane close {pane_id}
|
|
139
168
|
```
|
|
140
169
|
|
|
141
|
-
Only panes the plugin created, and only after §7.1's merge gates
|
|
170
|
+
Only panes/tabs/workspaces the plugin created, and only after §7.1's merge gates
|
|
171
|
+
have passed.
|
|
142
172
|
|
|
143
173
|
## Safety rules — never widened
|
|
144
174
|
|