@marcoscale98/piewf-herdr 5.14.1-fork.1

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 ADDED
@@ -0,0 +1,70 @@
1
+ # `@marcoscale98/piewf-herdr`
2
+
3
+ Connect workflow agents to Herdr panes. The extension adds contextual `/workflow` actions for live handoff and completed-session inspection, plus an optional mode that runs every workflow agent in Herdr.
4
+
5
+ [Full Herdr documentation](https://vekexasia.github.io/pi-extensible-workflows/herdr.html) | [Main package](https://github.com/vekexasia/pi-extensible-workflows#readme) | [Workflow operations](https://vekexasia.github.io/pi-extensible-workflows/developers.html#operations)
6
+
7
+ ## Requirements and installation
8
+
9
+ - Node.js 22.19 or newer.
10
+ - `@marcoscale98/pi-extensible-workflows` loaded in Pi.
11
+ - Pi running inside a Herdr-managed pane.
12
+ - Trusted package code and project context.
13
+
14
+ ```sh
15
+ pi install npm:@marcoscale98/pi-extensible-workflows@fork
16
+ pi install npm:@marcoscale98/piewf-herdr@fork
17
+ ```
18
+
19
+ The extension activates only when Herdr is available. It registers workflow attempt actions and a transport setup hook, not model-facing tools. Set `PI_CODING_AGENT_DIR` if the Pi agent directory is not `~/.pi/agent`.
20
+
21
+ ## Available `/workflow` actions
22
+
23
+ | Action | Available when | Behavior |
24
+ | --- | --- | --- |
25
+ | Open live session in Herdr pane | The latest attempt has a transferable live session and fully inspectable mode is off. | Suspends local ownership at a turn boundary, opens the same session in Herdr, and returns ownership after handback. |
26
+ | Open session in Herdr pane | The latest attempt is completed, failed, or cancelled and has a persisted session reference. | Opens the existing session in a separate Herdr pane for inspection without taking workflow ownership. |
27
+
28
+ Open `/workflow`, select a run, then select the agent attempt to see the applicable action.
29
+
30
+ ## Fully inspectable mode
31
+
32
+ Set the global workflow settings file, normally `~/.pi/agent/pi-extensible-workflows/settings.json`:
33
+
34
+ ```json
35
+ {
36
+ "extensionSettings": {
37
+ "herdr": {
38
+ "enableFullyInspectableMode": true
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ `extensionSettings.herdr` is the only valid location for Herdr configuration. The obsolete `extensions.herdr` object is rejected with `INVALID_SETTINGS`; migrate it before launching.
45
+
46
+ The effective value uses the global map unless a trusted project declares `extensionSettings`, in which case the project's complete map replaces it, including `{}`. Role settings are then applied by top-level-key replacement; nested objects and arrays are not merged. Untrusted project settings are ignored. When enabled, every workflow agent launches in a dedicated labeled Herdr workspace and the manual live-session action is hidden. Completed-session inspection remains available.
47
+
48
+ ## Live handoff and ownership
49
+
50
+ Live handoff pauses the local SDK at a turn boundary while the Herdr pane owns the task. A generated bridge reports Pi's `agent_settled` event directly to the transport, independently of user extension filters and Herdr screen detection. This returns ownership to the local SDK and closes the owned pane; accepting `workflow_result` alone does not end the execution. Pane exit, `/quit`, and cancellation remain fallback paths.
51
+
52
+ The extension preserves the originating Pi executable and entrypoint, model, tools, role/resource policy, context selection, prompt configuration, custom result tools, and transferable inline extensions. Temporary bridge files use private permissions; files and sockets are cleaned after the pane closes.
53
+
54
+ ## Interruption behavior and limitations
55
+
56
+ - Herdr's Pi screen detection cannot distinguish an aborted turn from a completed turn.
57
+ - In Pi's default keybindings, use double-`Escape` to abort a turn. A single `Ctrl-C` clears the editor and double-`Ctrl-C` exits Pi, so `Ctrl-C` is not a handback signal.
58
+ - Lifecycle reports from this extension are advisory when Herdr's built-in Pi integration has authority.
59
+ - A live handoff requires a transferable session file and an available originating Pi runtime.
60
+ - Inline extension factories are materialized as temporary explicit extensions in the Herdr pane.
61
+
62
+ ## Development
63
+
64
+ From the repository root:
65
+
66
+ ```sh
67
+ npm run build --workspace=packages/extensions/herdr
68
+ npm test --workspace=packages/extensions/herdr
69
+ npm run lint --workspace=packages/extensions/herdr
70
+ ```
@@ -0,0 +1,32 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentAttemptAction, AgentIdentity, AgentSetupHook, HerdrCommandRunner, HerdrWorkspacePane, HerdrWorkspacePaneRequest, WorkflowExtension, WorkflowRunContext } from "@marcoscale98/pi-extensible-workflows";
3
+ type WorkspaceManager = {
4
+ open(run: Readonly<WorkflowRunContext>, request: HerdrWorkspacePaneRequest): Promise<HerdrWorkspacePane>;
5
+ close(runId: string): Promise<void>;
6
+ closeAll(): Promise<void>;
7
+ };
8
+ type HerdrBreadcrumbIdentity = Omit<AgentIdentity, "structuralPath"> & {
9
+ structuralPath?: readonly string[];
10
+ };
11
+ export interface HerdrExtensionOptions {
12
+ env?: NodeJS.ProcessEnv;
13
+ runner?: HerdrCommandRunner;
14
+ workspaces?: WorkspaceManager;
15
+ agentDir?: string;
16
+ }
17
+ type HerdrExtensionOverrides = Pick<HerdrExtensionOptions, "env" | "runner" | "workspaces">;
18
+ export interface HerdrExtension extends WorkflowExtension {
19
+ agentAttemptActions: {
20
+ openSession: AgentAttemptAction;
21
+ openLiveSession: AgentAttemptAction;
22
+ };
23
+ agentSetupHooks: {
24
+ fullyInspectable: AgentSetupHook;
25
+ };
26
+ }
27
+ export declare function isFullyInspectableMode(agentDirectory?: string): boolean;
28
+ export declare function breadcrumbLabel(identity: Readonly<HerdrBreadcrumbIdentity>, attempt?: number): string;
29
+ export declare function createHerdrExtension(options?: HerdrExtensionOptions): HerdrExtension;
30
+ export declare function registerHerdrExtension(options?: HerdrExtensionOptions): boolean;
31
+ export default function extension(pi: ExtensionAPI, overrides?: HerdrExtensionOverrides): void;
32
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,851 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdtempSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { createServer } from "node:net";
5
+ import { join } from "node:path";
6
+ import { createExtensionRuntime, ExtensionRunner, ModelRegistry, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
7
+ import { WORKFLOW_BLOCKED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, createHerdrAgentReporter, createToolTimingExtension, errorText, herdrAvailable, herdrCommandRunner, loadSettings, openHerdrLivePane, waitForHerdrPane, registerWorkflowExtension, workflowSettingsPath, } from "@marcoscale98/pi-extensible-workflows";
8
+ function agentDir() { return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"); }
9
+ function settings(agentDirectory = agentDir()) {
10
+ try {
11
+ return loadSettings(workflowSettingsPath(agentDirectory));
12
+ }
13
+ catch {
14
+ return {};
15
+ }
16
+ }
17
+ function herdrConfig(agentDirectory = agentDir()) {
18
+ const workflowSettings = settings(agentDirectory);
19
+ return workflowSettings.extensionSettings?.herdr ?? {};
20
+ }
21
+ export function isFullyInspectableMode(agentDirectory = agentDir()) { return herdrConfig(agentDirectory).enableFullyInspectableMode === true; }
22
+ export function breadcrumbLabel(identity, attempt = 1) {
23
+ const parts = [
24
+ ...(identity.structuralPath ?? []),
25
+ ...(identity.parentBreadcrumb ? [identity.parentBreadcrumb] : []),
26
+ ...(identity.worktreeOwner ? [`worktree:${identity.worktreeOwner}`] : []),
27
+ identity.callSite,
28
+ ].filter((part) => typeof part === "string" && part.trim());
29
+ return `${parts.join(" > ")} #${String(attempt)}`;
30
+ }
31
+ function quote(value) { return `'${value.replace(/'/g, `'\\''`)}'`; }
32
+ function messageContent(message) { return message !== null && typeof message === "object" && !Array.isArray(message) ? message.content : undefined; }
33
+ function hasToolCall(message) { const content = messageContent(message); return Array.isArray(content) && content.some((part) => part !== null && typeof part === "object" && part.type === "toolCall"); }
34
+ function hasNamedToolCall(message, name) { const content = messageContent(message); return Array.isArray(content) && content.some((part) => part !== null && typeof part === "object" && part.type === "toolCall" && part.name === name); }
35
+ function needsContinuation(message) { return !message || message.stopReason === "aborted" || hasToolCall(message); }
36
+ function usableCwd(cwd) {
37
+ if (typeof cwd !== "string" || !cwd.trim())
38
+ return false;
39
+ try {
40
+ return statSync(cwd).isDirectory();
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ function completedSessionCwd(context) { return [context.attempt?.setup?.cwd, context.run?.cwd].find(usableCwd); }
47
+ function formatContextFiles(files) { return files.length ? `<project_context>\n\nProject-specific instructions and guidelines:\n\n${files.map(({ path, content }) => `<project_instructions path="${path}">\n${content}\n</project_instructions>`).join("\n\n")}\n</project_context>\n` : ""; }
48
+ function createCommandFiles(prepared, prompt, directPrompt, contextFiles) {
49
+ const paths = [];
50
+ const create = (kind, value) => {
51
+ const path = join(tmpdir(), `pi-herdr-${kind}-${String(process.pid)}-${randomBytes(6).toString("hex")}.txt`);
52
+ writeFileSync(path, value, { encoding: "utf8", mode: 0o600 });
53
+ paths.push(path);
54
+ return path;
55
+ };
56
+ const files = {
57
+ systemPrompt: prepared.systemPrompt === undefined ? undefined : create("system-prompt", prepared.systemPrompt),
58
+ appendPrompt: prepared.systemPromptAppend ? create("append-prompt", prepared.systemPromptAppend) : undefined,
59
+ contextPrompt: contextFiles && formatContextFiles(contextFiles) ? create("context-prompt", formatContextFiles(contextFiles)) : undefined,
60
+ prompt: prompt === undefined || directPrompt ? undefined : create("prompt", prompt),
61
+ settingsFile: create("settings", JSON.stringify(prepared.settings ?? {})),
62
+ };
63
+ return { ...files, command(value) { return `sh ${quote(create("command", `${value}\n`))}`; }, async close() { for (const path of paths) {
64
+ try {
65
+ unlinkSync(path);
66
+ }
67
+ catch { /* Cleanup is best effort after the child exits. */ }
68
+ } } };
69
+ }
70
+ function resourcePaths(session) {
71
+ const value = session.getHerdrResourcePaths?.();
72
+ if (!value || typeof value !== "object")
73
+ return { extensions: [], skills: [] };
74
+ return {
75
+ extensions: Array.isArray(value.extensions) ? value.extensions.filter((path) => typeof path === "string") : [],
76
+ skills: Array.isArray(value.skills) ? value.skills.filter((path) => typeof path === "string") : [],
77
+ };
78
+ }
79
+ function sessionPath(reference) {
80
+ const locator = reference?.locator;
81
+ return locator && typeof locator === "object" && !Array.isArray(locator) && typeof locator.sessionFile === "string" ? locator.sessionFile : undefined;
82
+ }
83
+ function materializeSessionForHandoff(session, prepared) {
84
+ const path = sessionPath(session.reference);
85
+ if (!path || existsSync(path))
86
+ return;
87
+ const manager = SessionManager.inMemory(prepared.cwd, session.reference.sessionId ? { id: session.reference.sessionId } : undefined);
88
+ manager.appendSessionInfo(prepared.sessionLabel);
89
+ const header = manager.getHeader();
90
+ if (!header)
91
+ throw new Error("Herdr cannot materialize a workflow session without a session header.");
92
+ const content = [header, ...manager.getEntries()].map((entry) => JSON.stringify(entry)).join("\n") + "\n";
93
+ try {
94
+ writeFileSync(path, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
95
+ }
96
+ catch (error) {
97
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST")
98
+ throw error;
99
+ }
100
+ }
101
+ async function createBridgeContext(session, prepared) {
102
+ const reference = session.reference;
103
+ const path = sessionPath(reference);
104
+ const sessionManager = path && existsSync(path) ? SessionManager.open(path, undefined, prepared.cwd) : SessionManager.inMemory(prepared.cwd, { id: reference.sessionId });
105
+ const shared = session.getHerdrModelContext?.();
106
+ let fallbackRegistry;
107
+ if (shared?.modelRegistry === undefined) {
108
+ const directory = prepared.agentDir ?? agentDir();
109
+ const modelRuntime = await ModelRuntime.create({ authPath: join(directory, "auth.json"), modelsPath: join(directory, "models.json") });
110
+ fallbackRegistry = new ModelRegistry(modelRuntime);
111
+ }
112
+ const modelRegistry = shared?.modelRegistry ?? fallbackRegistry;
113
+ if (modelRegistry === undefined)
114
+ throw new Error("Herdr cannot bridge tools without the local model registry.");
115
+ const model = shared?.model ?? modelRegistry.find(prepared.model.provider, prepared.model.model);
116
+ const runner = new ExtensionRunner([], createExtensionRuntime(), prepared.cwd, sessionManager, modelRegistry);
117
+ runner.setUIContext(undefined, "tui");
118
+ const baseContext = runner.createContext();
119
+ return (signal, abort) => new Proxy(baseContext, {
120
+ get(target, property, receiver) {
121
+ const current = session.getHerdrModelContext?.();
122
+ if (property === "model")
123
+ return current?.model ?? model;
124
+ if (property === "modelRegistry")
125
+ return current?.modelRegistry ?? modelRegistry;
126
+ if (property === "thinkingLevel")
127
+ return prepared.model.thinking;
128
+ if (property === "isIdle")
129
+ return () => false;
130
+ if (property === "isProjectTrusted")
131
+ return () => prepared.resourcePolicy?.projectTrusted ?? true;
132
+ if (property === "signal")
133
+ return signal;
134
+ if (property === "abort")
135
+ return abort;
136
+ const reflected = Reflect.get(target, property, receiver);
137
+ return reflected;
138
+ },
139
+ });
140
+ }
141
+ function inspectSessionCommand(reference) {
142
+ const source = sessionPath(reference) ?? reference.sessionId;
143
+ return `pi --session ${quote(source)} --tools ${quote("read,grep,find,ls")}`;
144
+ }
145
+ function inlineFactorySource(extension) {
146
+ const factory = typeof extension === "function" ? extension : typeof extension === "object" && typeof extension.factory === "function" ? extension.factory : undefined;
147
+ if (!factory)
148
+ throw new Error("Herdr live sessions cannot transfer an invalid inline extension factory.");
149
+ const source = Function.prototype.toString.call(factory);
150
+ if (source.includes("[native code]"))
151
+ throw new Error("Herdr live sessions cannot transfer a native inline extension factory.");
152
+ return source;
153
+ }
154
+ function createInlineExtensionBridge(prepared) {
155
+ const timingFactory = createToolTimingExtension();
156
+ const factories = prepared.extensionFactories?.includes(timingFactory) ? [...(prepared.extensionFactories ?? [])] : [...(prepared.extensionFactories ?? []), timingFactory];
157
+ const extensionPath = join(tmpdir(), `pi-herdr-extensions-${String(process.pid)}-${randomBytes(6).toString("hex")}.mjs`);
158
+ const source = `const factories = [${factories.map(inlineFactorySource).join(",")}];\nexport default async function(pi) { for (const factory of factories) await factory(pi); }\n`;
159
+ writeFileSync(extensionPath, source, { encoding: "utf8", mode: 0o600 });
160
+ return { extensionPath, async close() { try {
161
+ unlinkSync(extensionPath);
162
+ }
163
+ catch { /* Cleanup is best effort after the child exits. */ } } };
164
+ }
165
+ function isToolBridgeRequest(value) {
166
+ return value !== null && typeof value === "object" && typeof value.toolCallId === "string" && typeof value.name === "string";
167
+ }
168
+ function isLifecycleBridgeRequest(value) {
169
+ return value !== null && typeof value === "object" && value.type === "agent_settled";
170
+ }
171
+ function toolBridgeRequest(value) {
172
+ if (isToolBridgeRequest(value))
173
+ return value;
174
+ const name = value !== null && typeof value === "object" && !Array.isArray(value) ? value.name : undefined;
175
+ throw new Error(`Unknown Herdr tool: ${String(name)}`);
176
+ }
177
+ async function createToolBridge(session, prepared) {
178
+ const definitions = [...(prepared.customTools ?? []), ...(prepared.resultTool ? [prepared.resultTool] : [])];
179
+ const bridgeContext = definitions.length ? await createBridgeContext(session, prepared) : undefined;
180
+ const specs = definitions.map((definition) => {
181
+ const { name, label, description, promptSnippet, promptGuidelines, parameters, renderShell, executionMode } = definition;
182
+ return { name, label, description, ...(promptSnippet === undefined ? {} : { promptSnippet }), ...(promptGuidelines === undefined ? {} : { promptGuidelines }), parameters, ...(renderShell === undefined ? {} : { renderShell }), ...(executionMode === undefined ? {} : { executionMode }) };
183
+ });
184
+ // NOTE: mkdtemp creates the directory 0700, so only this user can connect to the socket regardless of umask.
185
+ const bridgeDirectory = mkdtempSync(join(tmpdir(), "pi-herdr-tools-"));
186
+ const socketPath = join(bridgeDirectory, "bridge.sock");
187
+ const extensionPath = join(bridgeDirectory, "bridge.mjs");
188
+ const source = `import net from "node:net";\nconst socketPath = ${JSON.stringify(socketPath)};\nconst tools = ${JSON.stringify(specs)};\nfunction callTool(toolCallId, name, params, signal, onUpdate) {\n return new Promise((resolve, reject) => {\n const socket = net.createConnection(socketPath);\n let buffer = "";\n let settled = false;\n const finish = (error, value) => { if (settled) return; settled = true; signal?.removeEventListener("abort", abort); socket.destroy(); error ? reject(error) : resolve(value); };\n const abort = () => finish(new Error("Herdr tool call aborted"));\n socket.setEncoding("utf8");\n socket.on("connect", () => socket.write(JSON.stringify({ toolCallId, name, params }) + "\\n"));\n socket.on("data", (chunk) => { buffer += chunk.toString(); let newline; while ((newline = buffer.indexOf("\\n")) >= 0) { const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); if (!line) continue; let message; try { message = JSON.parse(line); } catch { continue; } if (message.type === "update") onUpdate?.(message.value); else if (message.type === "error") finish(new Error(message.error)); else if (message.type === "result") finish(undefined, message.value); } });\n socket.on("error", (error) => finish(error));\n socket.on("close", () => finish(new Error("Herdr tool bridge closed")));\n signal?.addEventListener("abort", abort, { once: true });\n });\n}\nfunction reportSettled() {\n return new Promise((resolve) => {\n const socket = net.createConnection(socketPath);\n let buffer = "";\n let finished = false;\n const finish = () => { if (finished) return; finished = true; socket.destroy(); resolve(); };\n socket.setEncoding("utf8");\n socket.on("connect", () => socket.write(JSON.stringify({ type: "agent_settled" }) + "\\n"));\n socket.on("data", (chunk) => { buffer += chunk.toString(); let newline; while ((newline = buffer.indexOf("\\n")) >= 0) { const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1); if (!line) continue; try { if (JSON.parse(line).type === "ack") finish(); } catch {} } });\n socket.on("error", finish);\n socket.on("close", finish);\n });\n}\nexport default function(pi) { for (const tool of tools) pi.registerTool({ ...tool, async execute(toolCallId, params, signal, onUpdate) { return callTool(toolCallId, tool.name, params, signal, onUpdate); } }); pi.on("agent_settled", reportSettled); }\n`;
189
+ writeFileSync(extensionPath, source, { encoding: "utf8", mode: 0o600 });
190
+ const sockets = new Set();
191
+ let markSettled = () => { };
192
+ const settled = new Promise((resolve) => { markSettled = resolve; });
193
+ const server = createServer((socket) => {
194
+ sockets.add(socket);
195
+ const controller = new AbortController();
196
+ let buffer = "";
197
+ let handled = false;
198
+ let reportedSettled = false;
199
+ const send = (message) => { if (!socket.destroyed)
200
+ socket.write(`${JSON.stringify(message)}\n`); };
201
+ const handle = async (value) => {
202
+ if (handled)
203
+ return;
204
+ handled = true;
205
+ try {
206
+ if (isLifecycleBridgeRequest(value)) {
207
+ reportedSettled = true;
208
+ send({ type: "ack" });
209
+ return;
210
+ }
211
+ const request = toolBridgeRequest(value);
212
+ const definition = definitions.find(({ name }) => name === request.name);
213
+ if (!definition) {
214
+ send({ type: "error", error: `Unknown Herdr tool: ${request.name}` });
215
+ return;
216
+ }
217
+ if (!bridgeContext) {
218
+ send({ type: "error", error: "Herdr tool bridge context is unavailable" });
219
+ return;
220
+ }
221
+ const result = await definition.execute(request.toolCallId, request.params, controller.signal, (update) => { send({ type: "update", value: update }); }, bridgeContext(controller.signal, () => { controller.abort(); }));
222
+ send({ type: "result", value: result });
223
+ }
224
+ catch (error) {
225
+ send({ type: "error", error: errorText(error) });
226
+ }
227
+ };
228
+ const close = () => { controller.abort(); sockets.delete(socket); if (reportedSettled)
229
+ markSettled(); };
230
+ socket.setEncoding("utf8");
231
+ socket.on("data", (chunk) => { buffer += chunk.toString(); let newline; while ((newline = buffer.indexOf("\n")) >= 0) {
232
+ const line = buffer.slice(0, newline);
233
+ buffer = buffer.slice(newline + 1);
234
+ if (!line)
235
+ continue;
236
+ try {
237
+ void handle(JSON.parse(line));
238
+ }
239
+ catch (error) {
240
+ send({ type: "error", error: errorText(error) });
241
+ }
242
+ } });
243
+ socket.on("close", close);
244
+ socket.on("error", close);
245
+ });
246
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(socketPath, () => { server.removeListener("error", reject); resolve(); }); });
247
+ let closed = false;
248
+ return { extensionPath, settled, async close() {
249
+ if (closed)
250
+ return;
251
+ closed = true;
252
+ markSettled();
253
+ for (const socket of sockets)
254
+ socket.destroy();
255
+ await new Promise((resolve) => server.close(() => { resolve(); }));
256
+ rmSync(bridgeDirectory, { recursive: true, force: true });
257
+ } };
258
+ }
259
+ function sessionCommand(session, prepared, prompt, bridges, files, directPrompt) {
260
+ const runtime = prepared.piRuntime;
261
+ const entrypoint = runtime?.entrypoint;
262
+ if (!runtime || typeof runtime.executable !== "string" || !runtime.executable || entrypoint !== undefined && (typeof entrypoint !== "string" || !entrypoint)) {
263
+ const reason = typeof prepared.piRuntimeError === "string" && prepared.piRuntimeError ? ` (${prepared.piRuntimeError})` : "";
264
+ const error = new Error(`Herdr cannot launch workflow agent: originating Pi runtime is unavailable${reason}.`);
265
+ if (reason)
266
+ error.cause = new Error(prepared.piRuntimeError);
267
+ throw error;
268
+ }
269
+ const source = sessionPath(session.reference);
270
+ if (!source)
271
+ throw new Error("Herdr cannot hand off a live session without a transferable session file.");
272
+ const sessionArg = `--session ${quote(source)}`;
273
+ const model = `${prepared.model.provider}/${prepared.model.model}${prepared.model.thinking ? `:${prepared.model.thinking}` : ""}`;
274
+ const toolNames = [...new Set([...prepared.tools, ...(prepared.customTools ?? []).map(({ name }) => name), ...(prepared.resultTool ? [prepared.resultTool.name] : [])])];
275
+ const tools = toolNames.length ? ` --tools ${quote(toolNames.join(","))}` : " --no-tools";
276
+ const systemPrompt = prepared.systemPrompt !== undefined ? ` --system-prompt ${quote(files.systemPrompt ?? "")}` : prepared.systemPromptPath ? ` --system-prompt ${quote(prepared.systemPromptPath)}` : "";
277
+ const appendPrompt = files.appendPrompt ? ` --append-system-prompt ${quote(files.appendPrompt)}` : "";
278
+ const contextPrompt = files.contextPrompt ? ` --append-system-prompt ${quote(files.contextPrompt)}` : "";
279
+ const contextFiles = prepared.contextFiles === undefined ? "" : " --no-context-files";
280
+ const loaded = resourcePaths(session);
281
+ const allowedSkills = [...new Set([...(prepared.additionalSkillPaths ?? []), ...loaded.skills])];
282
+ const skills = prepared.resourcePolicy ? ` --no-skills${allowedSkills.map((path) => ` --skill ${quote(path)}`).join("")}` : allowedSkills.map((path) => ` --skill ${quote(path)}`).join("");
283
+ const allowedExtensions = [...new Set(loaded.extensions)];
284
+ const bridgeExtensions = bridges.filter(Boolean).map(({ extensionPath }) => ` --extension ${quote(extensionPath)}`).join("");
285
+ const extensions = prepared.resourcePolicy ? ` --no-extensions${allowedExtensions.map((path) => ` --extension ${quote(path)}`).join("")}${bridgeExtensions}` : bridgeExtensions;
286
+ const trust = prepared.resourcePolicy?.projectTrusted === false ? " --no-approve" : prepared.resourcePolicy?.projectTrusted === true ? " --approve" : "";
287
+ const environment = [prepared.agentDir ? `PI_CODING_AGENT_DIR=${quote(prepared.agentDir)}` : "", `PI_EXTENSIBLE_WORKFLOWS_SETTINGS_FILE=${quote(files.settingsFile)}`, "PI_EXTENSIBLE_WORKFLOWS_HERDR_OWNER=1"].filter(Boolean).join(" ");
288
+ const message = prompt === undefined ? "" : directPrompt ? ` ${quote(prompt)}` : ` @${quote(files.prompt ?? "")}`;
289
+ return `${environment} ${quote(runtime.executable)}${entrypoint ? ` ${quote(entrypoint)}` : ""} ${sessionArg} --model ${quote(model)}${tools}${systemPrompt}${appendPrompt}${contextPrompt}${contextFiles}${skills}${extensions}${trust}${message}`;
290
+ }
291
+ function paneId(value) { return typeof value === "string" ? value : value.paneId; }
292
+ function workspacePane(value) {
293
+ if (typeof value === "string")
294
+ throw new Error("Herdr did not create a workspace pane.");
295
+ return value;
296
+ }
297
+ function createWorkflowWorkspaces(runner) {
298
+ const workspaces = new Map();
299
+ return {
300
+ async open(run, request) {
301
+ const existing = workspaces.get(run.runId);
302
+ if (existing)
303
+ return workspacePane(await openHerdrLivePane({ ...request, workspaceId: await existing }, runner));
304
+ const opening = openHerdrLivePane({ ...request, workspaceLabel: `workflow ${run.workflow.name}` }, runner);
305
+ const workspace = opening.then((pane) => {
306
+ if (typeof pane === "string")
307
+ throw new Error("Herdr did not create a workspace pane.");
308
+ return pane.workspaceId;
309
+ });
310
+ workspaces.set(run.runId, workspace);
311
+ try {
312
+ return workspacePane(await opening);
313
+ }
314
+ catch (error) {
315
+ workspaces.delete(run.runId);
316
+ await workspace.catch(() => undefined);
317
+ throw error;
318
+ }
319
+ },
320
+ async close(runId) {
321
+ const workspace = workspaces.get(runId);
322
+ workspaces.delete(runId);
323
+ if (workspace)
324
+ await workspace.then((id) => runner(["workspace", "close", id])).catch(() => undefined);
325
+ },
326
+ async closeAll() { await Promise.all([...workspaces.keys()].map((runId) => this.close(runId))); },
327
+ };
328
+ }
329
+ async function launchPane({ session, prepared, identity, run, attempt, runner, fullyInspectable, env, signal, prompt, workspaces, tuiIndex, tuiLabel, directPrompt = false, onStatus }) {
330
+ const label = fullyInspectable && typeof tuiIndex === "number" && Number.isInteger(tuiIndex) && tuiIndex > 0 && typeof tuiLabel === "string" && tuiLabel.trim() ? `#${String(tuiIndex)} ${tuiLabel}` : fullyInspectable ? breadcrumbLabel(identity, attempt) : prepared.sessionLabel;
331
+ const bridge = await createToolBridge(session, prepared);
332
+ let inlineBridge;
333
+ let commandFiles;
334
+ let pane;
335
+ try {
336
+ inlineBridge = createInlineExtensionBridge(prepared);
337
+ const selectedContextFiles = prepared.contextFiles === undefined ? undefined : session.getHerdrContextFiles?.() ?? [];
338
+ commandFiles = createCommandFiles(prepared, prompt, directPrompt, selectedContextFiles);
339
+ const bridges = [inlineBridge, bridge].filter((value) => Boolean(value));
340
+ const command = commandFiles.command(sessionCommand(session, prepared, prompt, bridges, commandFiles, directPrompt));
341
+ let opened;
342
+ if (fullyInspectable) {
343
+ if (!workspaces || !run)
344
+ throw new Error("Herdr fully inspectable mode is missing its workspace context.");
345
+ opened = await workspaces.open(run, { cwd: prepared.cwd, tabLabel: label, command });
346
+ }
347
+ else {
348
+ opened = await openHerdrLivePane({ action: "live", cwd: prepared.cwd, command, ...(env.HERDR_PANE_ID ? { paneId: env.HERDR_PANE_ID } : {}) }, runner);
349
+ }
350
+ pane = paneId(opened);
351
+ if (!pane)
352
+ throw new Error("Herdr did not create a pane.");
353
+ let remoteClosed = false;
354
+ const openedPane = pane;
355
+ const closeRemote = async () => {
356
+ if (remoteClosed)
357
+ return;
358
+ remoteClosed = true;
359
+ await runner(fullyInspectable && typeof opened !== "string" ? ["tab", "close", opened.tabId] : ["pane", "close", openedPane]).catch(() => undefined);
360
+ };
361
+ const reporter = createHerdrAgentReporter(openedPane, label, runner);
362
+ const reference = session.reference;
363
+ const path = sessionPath(reference);
364
+ const sessionRef = path ? { sessionId: reference.sessionId, sessionPath: path } : { sessionId: reference.sessionId };
365
+ try {
366
+ await reporter.reportSession(sessionRef, "workflow-agent");
367
+ }
368
+ catch (error) {
369
+ await closeRemote();
370
+ throw error;
371
+ }
372
+ const monitorController = new AbortController();
373
+ const abortMonitor = () => { monitorController.abort(); };
374
+ if (signal.aborted)
375
+ abortMonitor();
376
+ else
377
+ signal.addEventListener("abort", abortMonitor, { once: true });
378
+ let cleanup;
379
+ const closeResources = () => cleanup ??= (async () => {
380
+ await reporter.release();
381
+ await bridge.close();
382
+ await inlineBridge?.close();
383
+ await commandFiles?.close();
384
+ })();
385
+ const monitor = waitForHerdrPane(openedPane, runner, { signal: monitorController.signal, ...(bridge.settled ? { settled: bridge.settled } : {}), ...(prepared.piRuntime?.entrypoint ? { originatingEntrypoint: prepared.piRuntime.entrypoint } : {}), ...(onStatus ? { onStatus } : {}) }).then(async (reason) => {
386
+ await closeRemote();
387
+ await closeResources();
388
+ return reason;
389
+ }).catch(async (error) => {
390
+ await closeRemote();
391
+ await closeResources().catch(() => undefined);
392
+ throw error;
393
+ }).finally(() => { signal.removeEventListener("abort", abortMonitor); });
394
+ return { pane: openedPane, monitor, reporter, closeRemote, close: async () => { monitorController.abort(); await monitor.then(() => undefined, () => undefined); } };
395
+ }
396
+ catch (error) {
397
+ if (pane && !fullyInspectable)
398
+ await runner(["pane", "close", pane]).catch(() => undefined);
399
+ await bridge.close();
400
+ await inlineBridge?.close();
401
+ await commandFiles?.close();
402
+ throw error;
403
+ }
404
+ }
405
+ function herdrTransport(agent, context, runner, fullyInspectable, env, workspaces) {
406
+ const local = agent.transport;
407
+ const transport = {
408
+ id: "herdr",
409
+ async createSession(prepared, sessionContext) {
410
+ const session = await local.createSession(prepared, sessionContext);
411
+ const suspendAndLaunch = async (prompt) => {
412
+ let suspended = false;
413
+ try {
414
+ if (session.suspendForHandoff) {
415
+ await session.suspendForHandoff();
416
+ suspended = true;
417
+ }
418
+ else {
419
+ await session.abort();
420
+ suspended = true;
421
+ }
422
+ materializeSessionForHandoff(session, prepared);
423
+ return await launchPane({ session, prepared, identity: context.identity, run: context.run, attempt: sessionContext.attempt, runner, fullyInspectable, env, signal: sessionContext.signal, prompt, workspaces, tuiIndex: context.tuiIndex, tuiLabel: context.tuiLabel });
424
+ }
425
+ catch (error) {
426
+ if (suspended) {
427
+ try {
428
+ await session.resumeFromHandoff?.();
429
+ }
430
+ catch { /* Preserve the pane launch failure. */ }
431
+ }
432
+ throw error;
433
+ }
434
+ };
435
+ const inFlight = new Set();
436
+ let disposed = false;
437
+ const isDisposed = () => disposed;
438
+ let sharedLaunch;
439
+ const beginLaunch = (prompt, retry = false) => {
440
+ if (isDisposed())
441
+ return Promise.reject(new Error("Herdr workflow session is disposed"));
442
+ // Joined callers share the first launch; the executor normally serializes prompts, so later text is not sent separately.
443
+ if (sharedLaunch)
444
+ return sharedLaunch;
445
+ const next = (async () => {
446
+ try {
447
+ return await suspendAndLaunch(prompt);
448
+ }
449
+ catch (error) {
450
+ if (!retry || isDisposed())
451
+ throw error;
452
+ return suspendAndLaunch(prompt);
453
+ }
454
+ })();
455
+ sharedLaunch = next;
456
+ inFlight.add(next);
457
+ void next.then(() => { inFlight.delete(next); if (sharedLaunch === next)
458
+ sharedLaunch = undefined; }, () => { inFlight.delete(next); if (sharedLaunch === next)
459
+ sharedLaunch = undefined; });
460
+ return next;
461
+ };
462
+ let opened;
463
+ try {
464
+ opened = await beginLaunch(prepared.initialPrompt);
465
+ }
466
+ catch (error) {
467
+ await session.dispose().catch(() => undefined);
468
+ throw error;
469
+ }
470
+ let disposal;
471
+ let active = opened;
472
+ return {
473
+ ...session,
474
+ getLastAssistant() { return session.getLastAssistant?.call(this); },
475
+ reference: { ...session.reference, transport: "herdr" },
476
+ async prompt(text) {
477
+ if (disposed)
478
+ throw new Error("Herdr workflow session is disposed");
479
+ let current = active;
480
+ if (!current) {
481
+ const pending = inFlight.values().next().value;
482
+ current = pending ? await pending : await beginLaunch(text, true);
483
+ }
484
+ active = current;
485
+ let monitorFailed = false;
486
+ let monitorError;
487
+ let resumeFailed = false;
488
+ let resumeError;
489
+ try {
490
+ await current.monitor;
491
+ }
492
+ catch (error) {
493
+ monitorFailed = true;
494
+ monitorError = error;
495
+ }
496
+ finally {
497
+ try {
498
+ await session.resumeFromHandoff?.();
499
+ }
500
+ catch (error) {
501
+ resumeFailed = true;
502
+ resumeError = error;
503
+ }
504
+ finally {
505
+ if (active === current)
506
+ active = undefined;
507
+ }
508
+ }
509
+ if (monitorFailed)
510
+ throw monitorError;
511
+ if (resumeFailed)
512
+ throw resumeError;
513
+ let assistant = session.getLastAssistant?.();
514
+ const resultTool = prepared.resultTool;
515
+ const resultSubmitted = resultTool !== undefined && hasNamedToolCall(assistant, resultTool.name);
516
+ const incomplete = needsContinuation(assistant);
517
+ if (!resultSubmitted && incomplete) {
518
+ await session.prompt("Continue the task from the current session state.");
519
+ assistant = session.getLastAssistant?.();
520
+ }
521
+ return assistant ? { assistant } : {};
522
+ },
523
+ async abort() { await session.abort(); },
524
+ async dispose() {
525
+ if (disposal) {
526
+ await disposal;
527
+ return;
528
+ }
529
+ disposed = true;
530
+ disposal = (async () => {
531
+ const pendingLaunches = [...inFlight];
532
+ if (pendingLaunches.length > 0) {
533
+ const results = await Promise.allSettled(pendingLaunches);
534
+ if (!active) {
535
+ for (const result of results) {
536
+ if (result.status === "fulfilled") {
537
+ active = result.value;
538
+ break;
539
+ }
540
+ }
541
+ }
542
+ }
543
+ if (active) {
544
+ await active.closeRemote();
545
+ await active.close();
546
+ }
547
+ await session.dispose();
548
+ })();
549
+ await disposal;
550
+ }
551
+ };
552
+ },
553
+ };
554
+ return transport;
555
+ }
556
+ async function abortSession(session) { await session.abort?.(); }
557
+ function hasExtensionHooks(value) {
558
+ if (value === null || typeof value !== "object")
559
+ return false;
560
+ const candidate = value;
561
+ return typeof candidate.on === "function" && typeof candidate.events?.on === "function";
562
+ }
563
+ function isHerdrBlockedEvent(value) {
564
+ return value !== null && typeof value === "object" && typeof value.active === "boolean" && (value.label === undefined || typeof value.label === "string");
565
+ }
566
+ function registerLifecycleHooks(pi, runner, env) {
567
+ const pane = env.HERDR_PANE_ID;
568
+ if (!pane || !hasExtensionHooks(pi))
569
+ return;
570
+ pi.events.on(WORKFLOW_BLOCKED_EVENT, (data) => { if (isHerdrBlockedEvent(data))
571
+ pi.events.emit("herdr:blocked", data); });
572
+ const liveRunIds = new Set();
573
+ const setLiveRun = (runId, active) => { const before = liveRunIds.size; if (active)
574
+ liveRunIds.add(runId);
575
+ else
576
+ liveRunIds.delete(runId); if (liveRunIds.size !== before)
577
+ pi.events.emit("herdr:working", { runId, active }); };
578
+ pi.events.on(WORKFLOW_RUN_STARTED_EVENT, (event) => { if (isRunEvent(event))
579
+ setLiveRun(event.runId, true); });
580
+ pi.events.on(WORKFLOW_RUN_RESUMED_EVENT, (event) => { if (isRunEvent(event))
581
+ setLiveRun(event.runId, true); });
582
+ pi.events.on(WORKFLOW_RUN_COMPLETED_EVENT, (event) => { if (isRunEvent(event))
583
+ setLiveRun(event.runId, false); });
584
+ pi.events.on(WORKFLOW_RUN_FAILED_EVENT, (event) => { if (isRunEvent(event))
585
+ setLiveRun(event.runId, false); });
586
+ pi.events.on(WORKFLOW_RUN_STATE_CHANGED_EVENT, (event) => {
587
+ if (!isRunEvent(event))
588
+ return;
589
+ const state = event.state;
590
+ if (state !== undefined)
591
+ setLiveRun(event.runId, isLiveRunActiveState(state));
592
+ });
593
+ if (env.PI_EXTENSIBLE_WORKFLOWS_HERDR_OWNER !== "1")
594
+ return;
595
+ const reporter = createHerdrAgentReporter(pane, "pi", runner);
596
+ let sessionRef = {};
597
+ let rootSession = false;
598
+ let agentActive = false;
599
+ let blockedCount = 0;
600
+ let blockedMessage;
601
+ let lastState;
602
+ let lastMessage;
603
+ let queuedState;
604
+ let stateReport = Promise.resolve();
605
+ const refresh = (ctx) => {
606
+ const path = ctx.sessionManager.getSessionFile();
607
+ const id = ctx.sessionManager.getSessionId();
608
+ sessionRef = { ...(typeof id === "string" ? { sessionId: id } : {}), ...(typeof path === "string" ? { sessionPath: path } : {}) };
609
+ return sessionRef;
610
+ };
611
+ const desiredState = () => blockedCount > 0 ? { state: "blocked", message: blockedMessage } : agentActive || liveRunIds.size > 0 ? { state: "working", message: undefined } : { state: "idle", message: undefined };
612
+ const drainState = async () => {
613
+ while (queuedState) {
614
+ const next = queuedState;
615
+ queuedState = undefined;
616
+ await reporter.reportState(next.state, next.message, sessionRef);
617
+ }
618
+ };
619
+ const publishState = (force = false) => {
620
+ const next = desiredState();
621
+ if (!force && next.state === lastState && next.message === lastMessage)
622
+ return;
623
+ lastState = next.state;
624
+ lastMessage = next.message;
625
+ queuedState = next;
626
+ stateReport = stateReport.then(drainState, drainState);
627
+ };
628
+ pi.events.on("herdr:working", () => { if (rootSession)
629
+ publishState(); });
630
+ pi.events.on("herdr:blocked", (data) => {
631
+ if (!rootSession)
632
+ return;
633
+ const blocked = isHerdrBlockedEvent(data) ? data : { active: false };
634
+ if (!blocked.active) {
635
+ blockedCount = Math.max(0, blockedCount - 1);
636
+ if (blockedCount === 0)
637
+ blockedMessage = undefined;
638
+ }
639
+ else {
640
+ blockedCount += 1;
641
+ blockedMessage = blocked.label;
642
+ }
643
+ publishState();
644
+ });
645
+ pi.on("session_start", async (event, ctx) => {
646
+ rootSession = true;
647
+ refresh(ctx);
648
+ await reporter.reportSession(sessionRef, event.reason);
649
+ agentActive = !ctx.isIdle();
650
+ publishState(true);
651
+ await stateReport;
652
+ });
653
+ pi.on("agent_start", async (_event, ctx) => {
654
+ if (!rootSession)
655
+ return;
656
+ refresh(ctx);
657
+ await reporter.reportSession(sessionRef);
658
+ agentActive = true;
659
+ publishState();
660
+ await stateReport;
661
+ });
662
+ pi.on("turn_end", async (event) => {
663
+ if (!rootSession)
664
+ return;
665
+ agentActive = hasToolCall(event.message) && !hasNamedToolCall(event.message, "workflow_result");
666
+ publishState();
667
+ await stateReport;
668
+ });
669
+ pi.on("agent_settled", async () => { if (!rootSession)
670
+ return; agentActive = false; publishState(); await stateReport; });
671
+ pi.on("agent_end", async (_event, ctx) => { if (!rootSession || !ctx.isIdle())
672
+ return; agentActive = false; publishState(); await stateReport; });
673
+ pi.on("session_shutdown", async (event) => { if (event.reason === "quit")
674
+ await reporter.release(); });
675
+ }
676
+ export function createHerdrExtension(options = {}) {
677
+ const env = options.env ?? process.env;
678
+ const runner = options.runner ?? herdrCommandRunner;
679
+ const workspaces = options.workspaces ?? createWorkflowWorkspaces(runner);
680
+ const fullyInspectable = isFullyInspectableMode(options.agentDir);
681
+ const fullyInspectableFor = (settings) => settings === undefined ? fullyInspectable : settings.herdr?.enableFullyInspectableMode === true;
682
+ const sessionCwd = (context) => completedSessionCwd("run" in context ? context : { attempt: context.attempt });
683
+ const actionIdentity = (context) => {
684
+ const parentBreadcrumb = "run" in context ? context.agent.parentBreadcrumb : undefined;
685
+ return { structuralPath: context.agent.structuralPath ?? [], ...(parentBreadcrumb ? { parentBreadcrumb } : {}), callSite: context.agent.label ?? context.agent.name, occurrence: context.attempt.attempt };
686
+ };
687
+ const openSessionVisible = (context) => herdrAvailable(env) && !context.liveSession && (context.agent.state === "completed" || context.agent.state === "failed" || context.agent.state === "cancelled" || context.agent.state === "stopped") && Boolean(context.session?.sessionId && sessionCwd(context));
688
+ const openSession = async (context) => {
689
+ const session = context.session;
690
+ const cwd = sessionCwd(context);
691
+ if (!session || context.liveSession || !cwd)
692
+ return;
693
+ await openHerdrLivePane({ action: "live", cwd, command: inspectSessionCommand(session), ...(env.HERDR_PANE_ID ? { paneId: env.HERDR_PANE_ID } : {}) }, runner);
694
+ };
695
+ const openLiveVisible = (context) => herdrAvailable(env) && !fullyInspectableFor(context.prepared?.settings) && Boolean(context.liveSession && sessionPath(context.liveSession.reference) && context.prepared && context.handoff);
696
+ const openLive = async (context) => {
697
+ const session = context.liveSession;
698
+ const prepared = context.prepared;
699
+ const handoff = context.handoff;
700
+ if (!session || !prepared || !handoff)
701
+ return;
702
+ if (!sessionPath(session.reference))
703
+ throw new Error("Herdr cannot hand off a live session without a transferable session file.");
704
+ const label = typeof context.agent.label === "string" && context.agent.label.trim() ? context.agent.label : typeof context.agent.name === "string" && context.agent.name.trim() ? context.agent.name : "workflow agent";
705
+ let lastWorkingMessage;
706
+ const setWorkingMessage = (state) => {
707
+ const message = state ? `${label}: ${state}` : undefined;
708
+ if (message === lastWorkingMessage)
709
+ return;
710
+ lastWorkingMessage = message;
711
+ context.ui.setWorkingMessage?.(message);
712
+ };
713
+ const handoffReleased = () => handoff.state === "completed";
714
+ const handoffCancelled = () => context.signal.aborted || handoffReleased();
715
+ setWorkingMessage("queued (waiting for a turn boundary)");
716
+ await handoff.request(async () => {
717
+ setWorkingMessage("opening pane");
718
+ const continueTask = needsContinuation(session.getLastAssistant());
719
+ let opened;
720
+ let suspended = false;
721
+ let reportedWorking = false;
722
+ let lastState = "working";
723
+ let displayedState;
724
+ const reportStatus = (state) => {
725
+ lastState = state;
726
+ if (state === "working")
727
+ reportedWorking = true;
728
+ if (displayedState !== state) {
729
+ displayedState = state;
730
+ setWorkingMessage(state);
731
+ }
732
+ };
733
+ try {
734
+ await abortSession(session);
735
+ if (handoffCancelled())
736
+ return;
737
+ if (session.suspendForHandoff)
738
+ await session.suspendForHandoff();
739
+ suspended = true;
740
+ if (handoffCancelled())
741
+ return;
742
+ opened = await launchPane({ session, prepared, identity: actionIdentity(context), attempt: context.attempt.attempt, runner, fullyInspectable: false, env, signal: context.signal, prompt: continueTask ? "Continue the current workflow task from this session." : undefined, directPrompt: continueTask, onStatus: reportStatus });
743
+ if (handoffCancelled()) {
744
+ if (handoffReleased())
745
+ await opened.closeRemote().catch(() => undefined);
746
+ await opened.monitor;
747
+ return;
748
+ }
749
+ handoff.takeover();
750
+ if (displayedState === undefined || lastState === "idle") {
751
+ displayedState = "working";
752
+ setWorkingMessage("working");
753
+ reportedWorking = true;
754
+ }
755
+ await opened.monitor;
756
+ }
757
+ finally {
758
+ try {
759
+ if (reportedWorking)
760
+ setWorkingMessage(lastState === "done" ? "completed" : "idle");
761
+ }
762
+ finally {
763
+ try {
764
+ if (suspended)
765
+ await session.resumeFromHandoff?.();
766
+ }
767
+ finally {
768
+ if (reportedWorking) {
769
+ await new Promise((resolve) => setTimeout(resolve, 50));
770
+ setWorkingMessage();
771
+ }
772
+ }
773
+ }
774
+ }
775
+ }).finally(() => { setWorkingMessage(); });
776
+ };
777
+ const validateSettings = (settings) => {
778
+ const value = settings?.herdr;
779
+ if (value === undefined)
780
+ return;
781
+ if (value === null || typeof value !== "object" || Array.isArray(value))
782
+ throw new Error("herdr settings must be an object");
783
+ const keys = Object.keys(value);
784
+ if (keys.some((key) => key !== "enableFullyInspectableMode"))
785
+ throw new Error("unsupported herdr setting");
786
+ if ("enableFullyInspectableMode" in value && typeof value.enableFullyInspectableMode !== "boolean")
787
+ throw new Error("enableFullyInspectableMode must be a boolean");
788
+ };
789
+ return {
790
+ version: "1.0.0",
791
+ headline: "Herdr workflow integration",
792
+ validateSettings,
793
+ agentAttemptActions: {
794
+ openSession: {
795
+ label: "Open session in Herdr pane",
796
+ visible: openSessionVisible,
797
+ run: openSession,
798
+ visibleStandalone: openSessionVisible,
799
+ runStandalone: openSession,
800
+ },
801
+ openLiveSession: {
802
+ label: "Open live session in Herdr pane",
803
+ visible: openLiveVisible,
804
+ run: openLive,
805
+ visibleStandalone: openLiveVisible,
806
+ runStandalone: openLive,
807
+ },
808
+ },
809
+ agentSetupHooks: {
810
+ fullyInspectable: {
811
+ setup(agent, context) {
812
+ if (context.mode === "inspection" || !fullyInspectableFor(context.settings) || !herdrAvailable(env))
813
+ return;
814
+ agent.transport = herdrTransport(agent, context, runner, true, env, workspaces);
815
+ },
816
+ },
817
+ },
818
+ };
819
+ }
820
+ export function registerHerdrExtension(options = {}) {
821
+ const env = options.env ?? process.env;
822
+ if (!herdrAvailable(env))
823
+ return false;
824
+ registerWorkflowExtension(createHerdrExtension(options));
825
+ return true;
826
+ }
827
+ function isRunEvent(value) {
828
+ return value !== null && typeof value === "object" && typeof value.runId === "string";
829
+ }
830
+ function isTerminalRunState(value) {
831
+ return value === "failed" || value === "stopped" || value === "interrupted" || value === "budget_exhausted";
832
+ }
833
+ function isLiveRunActiveState(value) { return value === "running" || value === "pausing"; }
834
+ function registerWorkspaceLifecycle(pi, workspaces) {
835
+ if (!hasExtensionHooks(pi))
836
+ return;
837
+ pi.events.on(WORKFLOW_RUN_COMPLETED_EVENT, (event) => { if (isRunEvent(event))
838
+ void workspaces.close(event.runId); });
839
+ pi.events.on(WORKFLOW_RUN_STATE_CHANGED_EVENT, (event) => { if (isRunEvent(event) && isTerminalRunState(event.state))
840
+ void workspaces.close(event.runId); });
841
+ pi.on("session_shutdown", async () => { await workspaces.closeAll(); });
842
+ }
843
+ export default function extension(pi, overrides = {}) {
844
+ const runner = overrides.runner ?? herdrCommandRunner;
845
+ const workspaces = overrides.workspaces ?? createWorkflowWorkspaces(runner);
846
+ const options = { env: overrides.env ?? process.env, runner, workspaces };
847
+ if (registerHerdrExtension(options)) {
848
+ registerLifecycleHooks(pi, runner, options.env);
849
+ registerWorkspaceLifecycle(pi, workspaces);
850
+ }
851
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@marcoscale98/piewf-herdr",
3
+ "version": "5.14.1-fork.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "rm -rf dist && tsc -p tsconfig.json",
7
+ "test": "npm run build && node --test --test-timeout=120000 --test-force-exit test/index.test.mjs",
8
+ "prepack": "npm run build",
9
+ "lint": "eslint ."
10
+ },
11
+ "pi": {
12
+ "extensions": ["./dist/index.js"]
13
+ },
14
+ "peerDependencies": {
15
+ "@earendil-works/pi-coding-agent": "*",
16
+ "@marcoscale98/pi-extensible-workflows": "*"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/marcoscale98/pi-extensible-workflows.git",
24
+ "directory": "packages/extensions/herdr"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "exports": {
28
+ ".": "./dist/index.js",
29
+ "./index.js": "./dist/index.js"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "devDependencies": {
35
+ "@earendil-works/pi-coding-agent": "0.85.0",
36
+ "@types/node": "24.12.4",
37
+ "typescript": "5.9.3"
38
+ }
39
+ }