allye-pi 1.6.2 → 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";
|
|
@@ -187,6 +217,11 @@ export function classifyWaitResult(result?: WaitExecutionResult, timeoutMs = 0,
|
|
|
187
217
|
const errorText = error instanceof Error ? error.message : String(error ?? "");
|
|
188
218
|
if (/abort/i.test(errorText)) return "aborted";
|
|
189
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";
|
|
190
225
|
if (!result || result.code !== 0) return "error";
|
|
191
226
|
return "completed";
|
|
192
227
|
}
|
|
@@ -200,7 +235,8 @@ export function shouldEmitWaitEvent(shuttingDown: boolean, settledWaits: Set<str
|
|
|
200
235
|
export function formatWaitEvent(event: WaitEvent): string {
|
|
201
236
|
const output = event.stdout?.trim() || "(no stdout)";
|
|
202
237
|
const error = event.error || event.stderr?.trim() || "(none)";
|
|
203
|
-
|
|
238
|
+
const prefix = event.kind === "intervened" ? "Herdr agent intervention detected" : "Herdr wait settled";
|
|
239
|
+
return `${prefix} for agent ${event.name}. Outcome: ${event.outcome}. `
|
|
204
240
|
+ `This is delegation evidence only, not a completion verdict. `
|
|
205
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`
|
|
206
242
|
+ `timeout_ms=${event.timeoutMs} code=${event.code ?? "n/a"} killed=${event.killed ?? false} timestamp=${event.timestamp}\n`
|
|
@@ -208,7 +244,7 @@ export function formatWaitEvent(event: WaitEvent): string {
|
|
|
208
244
|
}
|
|
209
245
|
|
|
210
246
|
export function waitEventNotificationLevel(event: WaitEvent): "info" | "warning" | "error" {
|
|
211
|
-
if (event.outcome === "error") return "error";
|
|
247
|
+
if (event.outcome === "error" || event.outcome === "blocked" || event.outcome === "unknown") return "error";
|
|
212
248
|
if (event.outcome === "timeout") return "warning";
|
|
213
249
|
return "info";
|
|
214
250
|
}
|
|
@@ -412,9 +448,30 @@ export function activeToolsForCapabilities(activeTools: string[], capabilities:
|
|
|
412
448
|
return [...next];
|
|
413
449
|
}
|
|
414
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
|
+
|
|
415
472
|
export function dispatchStateIsAcceptable(stateText: string): boolean {
|
|
416
|
-
|
|
417
|
-
return
|
|
473
|
+
const state = extractAgentLifecycleState(parseJson(stateText));
|
|
474
|
+
return state === "working" || state === "idle" || state === "done";
|
|
418
475
|
}
|
|
419
476
|
|
|
420
477
|
function requireHerdr(capabilities: AllyeCapabilities): void {
|
|
@@ -428,6 +485,23 @@ function requireAbsolutePath(value: unknown, name: string): string {
|
|
|
428
485
|
return value;
|
|
429
486
|
}
|
|
430
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
|
+
|
|
431
505
|
async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<string, unknown>, registerWait: WaitRegistrar, cancelWait: WaitCanceller, ownership: DelegationState): Promise<unknown> {
|
|
432
506
|
const operation = params.operation;
|
|
433
507
|
if (operation === "detect") {
|
|
@@ -442,6 +516,45 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
442
516
|
|
|
443
517
|
requireHerdr(capabilities);
|
|
444
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
|
+
|
|
445
558
|
if (operation === "spawn") {
|
|
446
559
|
const cwd = requireAbsolutePath(params.cwd, "cwd");
|
|
447
560
|
const worktree = requireAbsolutePath(params.worktree, "worktree");
|
|
@@ -462,9 +575,11 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
462
575
|
&& candidate.workspaceId === String(layoutMetadata.workspace_id ?? candidate.workspaceId)
|
|
463
576
|
&& candidate.tabId === String(layoutMetadata.tab_id ?? candidate.tabId));
|
|
464
577
|
if (!record) throw new Error("Herdr returned a pane without workspace/tab ownership metadata");
|
|
465
|
-
|
|
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 });
|
|
466
581
|
await waitForInteractiveShell(newPane);
|
|
467
|
-
return { pane: newPane, worktree, cwd, direction, ready: true, ownership: record };
|
|
582
|
+
return { pane: newPane, worktree, cwd, direction, ready: true, ownership: { ...record, resourceId } };
|
|
468
583
|
}
|
|
469
584
|
|
|
470
585
|
if (operation === "dispatch") {
|
|
@@ -496,7 +611,23 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
496
611
|
// A fast agent can already be idle/done by the time it is inspected. The
|
|
497
612
|
// server-owned wait is registered immediately so that completion is not
|
|
498
613
|
// lost and a fast, successful turn is not falsely rejected.
|
|
499
|
-
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);
|
|
500
631
|
let agentState: unknown;
|
|
501
632
|
try {
|
|
502
633
|
agentState = herdrResult(await runHerdr(["agent", "get", name]));
|
|
@@ -514,10 +645,33 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
514
645
|
started: herdrResult(start),
|
|
515
646
|
state: agentState,
|
|
516
647
|
wait,
|
|
648
|
+
execution,
|
|
517
649
|
ownership: ownedPane,
|
|
518
650
|
};
|
|
519
651
|
}
|
|
520
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
|
+
|
|
521
675
|
if (operation === "wait") {
|
|
522
676
|
const name = params.name;
|
|
523
677
|
if (typeof name !== "string" || !AGENT_NAME.test(name)) throw new Error("valid agent name is required");
|
|
@@ -526,6 +680,14 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
526
680
|
return registerWait(name, timeout);
|
|
527
681
|
}
|
|
528
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
|
+
|
|
529
691
|
if (operation === "collect") {
|
|
530
692
|
const storyId = params.storyId;
|
|
531
693
|
if (typeof storyId !== "string" || storyId.length === 0) throw new Error("collect requires the story UUID as storyId");
|
|
@@ -534,7 +696,7 @@ async function runtimeOperation(capabilities: AllyeCapabilities, params: Record<
|
|
|
534
696
|
const review = await callAllye("allye_intelligence", { action: "memory_search", query: reviewQuery, limit: 10, return_content: true });
|
|
535
697
|
const implementationQuery = typeof params.taskKey === "string" ? `Implementation ${params.taskKey}` : `Implementation ${storyId}`;
|
|
536
698
|
const implementation = await callAllye("allye_intelligence", { action: "memory_search", query: implementationQuery, limit: 10, return_content: true });
|
|
537
|
-
return { storyChildren: story, review, implementation };
|
|
699
|
+
return { storyChildren: story, review, implementation, cleanupAvailable: typeof params.executionId === "string" };
|
|
538
700
|
}
|
|
539
701
|
|
|
540
702
|
throw new Error(`Unknown runtime operation: ${String(operation)}`);
|
|
@@ -546,15 +708,19 @@ function registerRuntimeTool(pi: ExtensionAPI, getCapabilities: () => AllyeCapab
|
|
|
546
708
|
label: "Allye Runtime",
|
|
547
709
|
description: "Use optional Herdr capabilities for delegation, waiting, and collecting results when available.",
|
|
548
710
|
parameters: Type.Object({
|
|
549
|
-
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" }),
|
|
550
712
|
cwd: Type.Optional(Type.String({ description: "Absolute plugin-enabled repository root for spawn" })),
|
|
551
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" })),
|
|
552
717
|
direction: Type.Optional(Type.String({ description: "right or down" })),
|
|
553
718
|
pane: Type.Optional(Type.String({ description: "Pane id returned by Herdr" })),
|
|
554
719
|
name: Type.Optional(Type.String({ description: "Stable Herdr agent name" })),
|
|
555
720
|
kind: Type.Optional(Type.String({ description: "Herdr agent kind, normally pi" })),
|
|
556
721
|
briefing: Type.Optional(Type.String({ description: "Complete story briefing, including the absolute worktree path" })),
|
|
557
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" })),
|
|
558
724
|
storyId: Type.Optional(Type.String({ description: "Allye story UUID for collect" })),
|
|
559
725
|
storyKey: Type.Optional(Type.String({ description: "Story key for review memory search" })),
|
|
560
726
|
taskKey: Type.Optional(Type.String({ description: "Task key for implementation memory search" })),
|
|
@@ -580,9 +746,9 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
580
746
|
let activeContext: ExtensionContext | undefined;
|
|
581
747
|
const waits = new Map<string, AbortController>();
|
|
582
748
|
const settledWaits = new Set<string>();
|
|
583
|
-
const ownership: DelegationState = { panes: [], waits: new Set() };
|
|
749
|
+
const ownership: DelegationState = { panes: [], resources: new Map(), waits: new Set(), executions: new Map() };
|
|
584
750
|
|
|
585
|
-
const registerWait: WaitRegistrar = (name, timeoutMs) => {
|
|
751
|
+
const registerWait: WaitRegistrar = (name, timeoutMs, execution) => {
|
|
586
752
|
if (settledWaits.has(name)) return { registered: false, name, timeoutMs, duplicate: true, settled: true };
|
|
587
753
|
const existing = waits.get(name);
|
|
588
754
|
if (existing) return { registered: true, name, timeoutMs, duplicate: true };
|
|
@@ -593,6 +759,8 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
593
759
|
signal: controller.signal,
|
|
594
760
|
}).then((result) => {
|
|
595
761
|
const event: WaitEvent = {
|
|
762
|
+
kind: execution?.manualIntervention ? "intervened" : "settled",
|
|
763
|
+
executionId: execution?.executionId,
|
|
596
764
|
name,
|
|
597
765
|
timeoutMs,
|
|
598
766
|
outcome: classifyWaitResult(result, timeoutMs),
|
|
@@ -602,6 +770,11 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
602
770
|
stderr: result.stderr,
|
|
603
771
|
timestamp: new Date().toISOString(),
|
|
604
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
|
+
}
|
|
605
778
|
deliverWaitEvent(event, shuttingDown, settledWaits, {
|
|
606
779
|
appendEntry: (customType, value) => pi.appendEntry(customType, value),
|
|
607
780
|
notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
|
|
@@ -609,12 +782,19 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
609
782
|
});
|
|
610
783
|
}).catch((error) => {
|
|
611
784
|
const event: WaitEvent = {
|
|
785
|
+
kind: execution?.manualIntervention ? "intervened" : "settled",
|
|
786
|
+
executionId: execution?.executionId,
|
|
612
787
|
name,
|
|
613
788
|
timeoutMs,
|
|
614
789
|
outcome: classifyWaitResult(undefined, timeoutMs, error),
|
|
615
790
|
error: error instanceof Error ? error.message : String(error),
|
|
616
791
|
timestamp: new Date().toISOString(),
|
|
617
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
|
+
}
|
|
618
798
|
deliverWaitEvent(event, shuttingDown, settledWaits, {
|
|
619
799
|
appendEntry: (customType, value) => pi.appendEntry(customType, value),
|
|
620
800
|
notify: activeContext?.hasUI ? (message, level) => activeContext?.ui.notify(message, level) : undefined,
|
|
@@ -664,7 +844,9 @@ export default function allyePiAdapter(pi: ExtensionAPI): void {
|
|
|
664
844
|
for (const controller of waits.values()) controller.abort();
|
|
665
845
|
waits.clear();
|
|
666
846
|
ownership.waits.clear();
|
|
667
|
-
|
|
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.
|
|
668
850
|
});
|
|
669
851
|
|
|
670
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
|
|