pi-subagents 0.66.0 → 0.67.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/CHANGELOG.md +68 -0
- package/README.md +4 -3
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +6 -3
- package/docs/configuration.md +7 -5
- package/docs/extension-api.md +33 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +1 -1
- package/docs/observability.md +4 -4
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +18 -8
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +26 -12
- package/inspector-runner.mjs +2 -2
- package/package.json +1 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +7 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agents.ts +6 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +2 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +20 -8
- package/src/extension/public-execution.ts +4 -2
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +67 -78
- package/src/extension/tool-description.ts +29 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +22 -13
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/auto-drain.ts +6 -3
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +31 -1
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/run-child-session.ts +61 -4
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +11 -3
- package/src/runs/background/runner-child-launch.ts +2 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +47 -31
- package/src/runs/background/subagent-wait.ts +51 -8
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/execution.ts +44 -31
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +110 -100
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +5 -2
- package/src/runs/shared/async-status-projection.ts +4 -0
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch.ts +12 -2
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +33 -2
- package/src/runs/shared/child-tool-plan.ts +122 -3
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-fallback.ts +2 -0
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +9 -3
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree.ts +42 -12
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +4 -1
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +149 -28
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/scripted-workflow.ts +237 -7
- package/src/workflows/workflow-checklist.ts +2 -2
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { execFile, type ExecFileOptionsWithStringEncoding } from "node:child_process";
|
|
2
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { Details } from "../../shared/types.ts";
|
|
4
|
+
import type { InspectorContext, InspectorLaunch, InspectorParams } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
/** Static Ghostty 1.3 AppleScript; dynamic values arrive as `on run argv` arguments. */
|
|
7
|
+
export const GHOSTTY_APPLESCRIPT = `on run argv
|
|
8
|
+
set launchCommand to item 1 of argv
|
|
9
|
+
set launchCwd to item 2 of argv
|
|
10
|
+
set shouldFocus to item 3 of argv
|
|
11
|
+
tell application "Ghostty"
|
|
12
|
+
set sourceWindow to front window
|
|
13
|
+
set sourceTab to selected tab of sourceWindow
|
|
14
|
+
set sourceTerminal to focused terminal of sourceTab
|
|
15
|
+
set surfaceConfiguration to new surface configuration
|
|
16
|
+
set initial working directory of surfaceConfiguration to launchCwd
|
|
17
|
+
set command of surfaceConfiguration to launchCommand
|
|
18
|
+
set newTerminal to split sourceTerminal direction right with configuration surfaceConfiguration
|
|
19
|
+
if shouldFocus is "true" then
|
|
20
|
+
focus newTerminal
|
|
21
|
+
else
|
|
22
|
+
focus sourceTerminal
|
|
23
|
+
end if
|
|
24
|
+
return id of newTerminal
|
|
25
|
+
end tell
|
|
26
|
+
end run`;
|
|
27
|
+
|
|
28
|
+
export type GhosttyRunner = (args: readonly string[], options: ExecFileOptionsWithStringEncoding) => Promise<{ stdout: string; stderr: string }>;
|
|
29
|
+
|
|
30
|
+
function defaultRunner(args: readonly string[], options: ExecFileOptionsWithStringEncoding): Promise<{ stdout: string; stderr: string }> {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
execFile("/usr/bin/osascript", [...args], options, (error, stdout, stderr) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
35
|
+
reject(failure);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve({ stdout, stderr });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
44
|
+
const response: AgentToolResult<Details> = {
|
|
45
|
+
content: [{ type: "text", text }],
|
|
46
|
+
details: { mode: "management", results: [] },
|
|
47
|
+
};
|
|
48
|
+
if (isError) response.isError = true;
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const failureHint = "Ghostty inspector requires Ghostty 1.3+ and Automation permission for osascript.";
|
|
53
|
+
|
|
54
|
+
export async function openGhosttyInspector(
|
|
55
|
+
context: InspectorContext,
|
|
56
|
+
launch: InspectorLaunch,
|
|
57
|
+
params: InspectorParams,
|
|
58
|
+
runner: GhosttyRunner = defaultRunner,
|
|
59
|
+
): Promise<AgentToolResult<Details>> {
|
|
60
|
+
try {
|
|
61
|
+
const output = await runner(["-e", GHOSTTY_APPLESCRIPT, "--", launch.displayCommand, context.target.status.cwd ?? context.cwd, params.focus === true ? "true" : "false"], {
|
|
62
|
+
signal: context.signal,
|
|
63
|
+
timeout: 15_000,
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
maxBuffer: 64 * 1024,
|
|
66
|
+
});
|
|
67
|
+
const id = output.stdout.trim();
|
|
68
|
+
if (!id) return result(`Ghostty inspector error: Ghostty returned an empty terminal id. ${failureHint}`, true);
|
|
69
|
+
return result(`Opened read-only Ghostty inspector terminal ${id} for async run ${context.target.runId}. Status and close are unavailable because this plugin writes no binding.`);
|
|
70
|
+
} catch (cause) {
|
|
71
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
72
|
+
return result(`Ghostty inspector error: ${message}. ${failureHint}`, true);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { openGhosttyInspector, type GhosttyRunner } from "./actions.ts";
|
|
2
|
+
import type { InspectorPlugin } from "../types.ts";
|
|
3
|
+
|
|
4
|
+
export interface GhosttyPluginDeps {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
runner?: GhosttyRunner;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createGhosttyInspectorPlugin(deps: GhosttyPluginDeps = {}): InspectorPlugin {
|
|
10
|
+
const platform = deps.platform ?? process.platform;
|
|
11
|
+
return {
|
|
12
|
+
name: "ghostty",
|
|
13
|
+
available: (context) => platform === "darwin" && context.env.TERM_PROGRAM?.toLowerCase() === "ghostty",
|
|
14
|
+
owns: () => false,
|
|
15
|
+
open: (context, launch, params) => openGhosttyInspector(context, launch, params, deps.runner),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
|
-
import { readMissionBinding } from "../../missions/lifecycle.ts";
|
|
6
|
-
import { listMissions, missionRecordPath, resolveMissionStoreLocation } from "../../missions/store.ts";
|
|
7
|
-
import type { MissionStoreConfig } from "../../missions/types.ts";
|
|
8
|
-
import { resolveAuthorityDecision, type AuthorityPolicyConfig } from "../../policy/authority.ts";
|
|
9
4
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
14
|
-
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
|
|
15
|
-
import { encodeSessionRoots } from "./session-roots-codec.ts";
|
|
16
|
-
import { formatShellCommand } from "./shell-command.ts";
|
|
17
|
-
|
|
18
|
-
export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
|
|
19
|
-
export type HerdrInspectorAction = typeof HERDR_INSPECTOR_ACTIONS[number];
|
|
5
|
+
import type { Details } from "../../shared/types.ts";
|
|
6
|
+
import type { InspectorContext, InspectorLaunch, InspectorParams, InspectorTarget } from "../types.ts";
|
|
7
|
+
import { detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
|
|
20
8
|
|
|
21
9
|
export interface HerdrInspectorBinding {
|
|
22
10
|
schemaVersion: 1;
|
|
@@ -33,203 +21,135 @@ export interface HerdrInspectorBinding {
|
|
|
33
21
|
command: string;
|
|
34
22
|
}
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
id?: string;
|
|
38
|
-
runId?: string;
|
|
39
|
-
dir?: string;
|
|
40
|
-
index?: number;
|
|
41
|
-
focus?: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface InspectorDeps {
|
|
45
|
-
state?: SubagentState;
|
|
46
|
-
asyncDirRoot?: string;
|
|
47
|
-
resultsDir?: string;
|
|
48
|
-
client?: HerdrClient;
|
|
49
|
-
missions?: MissionStoreConfig;
|
|
50
|
-
authorityPolicy?: AuthorityPolicyConfig;
|
|
51
|
-
sessionRoots?: string[];
|
|
52
|
-
cwd: string;
|
|
53
|
-
signal?: AbortSignal;
|
|
54
|
-
now?: () => Date;
|
|
55
|
-
runnerPath?: string;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
59
|
-
return { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}), details: { mode: "management", results: [] } };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function formatHerdrError(input: { code: HerdrErrorCode; message: string }): string {
|
|
63
|
-
return `Herdr inspector error (${input.code}): ${input.message}`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function bindingPath(asyncDir: string, index?: number): string {
|
|
24
|
+
export function bindingPath(asyncDir: string, index?: number): string {
|
|
67
25
|
return path.join(asyncDir, "inspectors", `herdr${index === undefined ? "" : `-${index}`}.json`);
|
|
68
26
|
}
|
|
69
27
|
|
|
70
|
-
function
|
|
28
|
+
function parse(value: unknown): HerdrInspectorBinding | undefined {
|
|
71
29
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
30
|
+
const binding = value as Partial<HerdrInspectorBinding>;
|
|
31
|
+
if (binding.schemaVersion !== 1
|
|
32
|
+
|| binding.kind !== "herdr-inspector"
|
|
33
|
+
|| (binding.childIndex !== undefined && (!Number.isInteger(binding.childIndex) || binding.childIndex < 0))
|
|
34
|
+
|| typeof binding.runId !== "string"
|
|
35
|
+
|| typeof binding.asyncDir !== "string"
|
|
36
|
+
|| typeof binding.paneId !== "string"
|
|
37
|
+
|| typeof binding.openedAt !== "string"
|
|
38
|
+
|| typeof binding.command !== "string") return undefined;
|
|
39
|
+
return binding as HerdrInspectorBinding;
|
|
77
40
|
}
|
|
78
41
|
|
|
79
42
|
export function readHerdrInspectorBinding(asyncDir: string, index?: number): HerdrInspectorBinding | undefined {
|
|
80
|
-
try { return parseBinding(JSON.parse(fs.readFileSync(bindingPath(asyncDir, index), "utf-8"))); } catch { return undefined; }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function extractPaneId(value: unknown): string | undefined {
|
|
84
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
85
|
-
const record = value as Record<string, unknown>;
|
|
86
|
-
const pane = record.pane && typeof record.pane === "object" && !Array.isArray(record.pane) ? record.pane as Record<string, unknown> : record;
|
|
87
|
-
for (const key of ["pane_id", "paneId", "id"]) if (typeof pane[key] === "string") return pane[key];
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean; sessionRoots: string[] }): string {
|
|
92
|
-
const args = [input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop), "--session-roots", encodeSessionRoots(input.sessionRoots)];
|
|
93
|
-
if (input.index !== undefined) args.push("--index", String(input.index));
|
|
94
|
-
if (input.missionPath) args.push("--mission-path", input.missionPath);
|
|
95
|
-
return formatShellCommand(resolveNodeExecutable(), args);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function missionForRun(asyncDir: string, cwd: string, config: MissionStoreConfig | undefined, runId: string): { id: string; path: string } | undefined {
|
|
99
43
|
try {
|
|
100
|
-
|
|
101
|
-
if (binding) return { id: binding.missionId, path: missionRecordPath(binding.location, binding.missionId) };
|
|
102
|
-
const location = resolveMissionStoreLocation({ projectRoot: cwd, ...(config ? { config } : {}) });
|
|
103
|
-
const mission = listMissions(location).records.find((record) => record.runs.some((run) => run.runId === runId));
|
|
104
|
-
return mission ? { id: mission.id, path: missionRecordPath(location, mission.id) } : undefined;
|
|
44
|
+
return parse(JSON.parse(fs.readFileSync(bindingPath(asyncDir, index), "utf8")));
|
|
105
45
|
} catch {
|
|
106
46
|
return undefined;
|
|
107
47
|
}
|
|
108
48
|
}
|
|
109
49
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function herdrSessionRoots(target: { runId: string }, deps: InspectorDeps): string[] {
|
|
117
|
-
const roots = deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? [];
|
|
118
|
-
const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
|
|
119
|
-
return [...new Set([...roots, ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {
|
|
50
|
+
/** Read a binding only when it belongs to the requested inspector target. */
|
|
51
|
+
export function readHerdrInspectorBindingForTarget(target: InspectorTarget): HerdrInspectorBinding | undefined {
|
|
52
|
+
const binding = readHerdrInspectorBinding(target.asyncDir, target.index);
|
|
53
|
+
if (!binding || binding.runId !== target.runId || binding.childIndex !== target.index) return undefined;
|
|
123
54
|
try {
|
|
124
|
-
if (fs.
|
|
125
|
-
const realDir = fs.realpathSync(asyncDir);
|
|
126
|
-
const registered = [...(deps.state?.asyncJobs.values() ?? [])].some((job) => {
|
|
127
|
-
try { return fs.realpathSync(job.asyncDir) === realDir; } catch { return false; }
|
|
128
|
-
});
|
|
129
|
-
if (registered) return true;
|
|
130
|
-
const root = deps.asyncDirRoot ?? DIRS.async;
|
|
131
|
-
if (!fs.existsSync(root) || !pathWithin(root, asyncDir)) return false;
|
|
132
|
-
return pathWithin(fs.realpathSync(root), realDir);
|
|
55
|
+
if (fs.realpathSync(binding.asyncDir) !== fs.realpathSync(target.asyncDir)) return undefined;
|
|
133
56
|
} catch {
|
|
134
|
-
return
|
|
57
|
+
return undefined;
|
|
135
58
|
}
|
|
59
|
+
return binding;
|
|
136
60
|
}
|
|
137
61
|
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!status) return { error: `No async run status found in '${asyncDir}'.` };
|
|
145
|
-
if (requestedId && requestedId !== status.runId && !status.runId.startsWith(requestedId)) return { error: `Run '${requestedId}' does not match status run '${status.runId}'.` };
|
|
146
|
-
return { runId: status.runId, asyncDir };
|
|
147
|
-
}
|
|
148
|
-
if (!requestedId) return { error: "Herdr inspector actions require id or dir." };
|
|
149
|
-
try {
|
|
150
|
-
const resolved = resolveSubagentRunId(requestedId, { state: deps.state, asyncDirRoot: deps.asyncDirRoot ?? DIRS.async, resultsDir: deps.resultsDir ?? DIRS.results });
|
|
151
|
-
if (!resolved) return { error: `No subagent run found for '${requestedId}'.` };
|
|
152
|
-
if (resolved.kind !== "async" || !resolved.location.asyncDir) return { error: `Run '${resolved.id}' is not an inspectable async run with lifecycle artifacts.` };
|
|
153
|
-
return { runId: resolved.id, asyncDir: resolved.location.asyncDir };
|
|
154
|
-
} catch (cause) {
|
|
155
|
-
return { error: cause instanceof Error ? cause.message : String(cause) };
|
|
156
|
-
}
|
|
62
|
+
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text }],
|
|
65
|
+
...(isError ? { isError: true } : {}),
|
|
66
|
+
details: { mode: "management", results: [] },
|
|
67
|
+
};
|
|
157
68
|
}
|
|
158
69
|
|
|
159
|
-
|
|
160
|
-
return
|
|
70
|
+
function errorText(input: { code: HerdrErrorCode; message: string }): string {
|
|
71
|
+
return `Herdr inspector error (${input.code}): ${input.message}`;
|
|
161
72
|
}
|
|
162
73
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return result(`Async run '${target.runId}' has ${status.steps?.length ?? 0} children. Index ${params.index} is out of range.`, true);
|
|
170
|
-
}
|
|
171
|
-
const existing = readHerdrInspectorBinding(target.asyncDir, params.index);
|
|
172
|
-
const client = deps.client ?? createHerdrClient();
|
|
173
|
-
|
|
174
|
-
if (action === "inspector.status") {
|
|
175
|
-
if (!existing) return result(`No Herdr inspector binding exists for async run ${target.runId}${params.index === undefined ? "" : ` child ${params.index}`}.`);
|
|
176
|
-
const live = await paneExists(client, existing.paneId, deps.signal);
|
|
177
|
-
if (live.ok === false) return result(`${formatHerdrError(live.error)}\nBinding: ${bindingPath(target.asyncDir, params.index)}\nRun state remains authoritative: ${status.state}.`, true);
|
|
178
|
-
return result(`Herdr inspector ${existing.paneId} is open for async run ${target.runId}.\nRun state: ${status.state}\nBinding: ${bindingPath(target.asyncDir, params.index)}`);
|
|
74
|
+
function paneId(value: unknown): string | undefined {
|
|
75
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
76
|
+
const record = value as Record<string, unknown>;
|
|
77
|
+
const pane = record.pane && typeof record.pane === "object" ? record.pane as Record<string, unknown> : record;
|
|
78
|
+
for (const key of ["pane_id", "paneId", "id"]) {
|
|
79
|
+
if (typeof pane[key] === "string") return pane[key];
|
|
179
80
|
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
180
83
|
|
|
84
|
+
async function pane(client: HerdrClient, id: string, signal?: AbortSignal): Promise<HerdrResult<unknown>> {
|
|
85
|
+
return client.run(["pane", "get", id], { timeoutMs: 5_000, signal });
|
|
86
|
+
}
|
|
181
87
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
if (detected.ok === false) return result(formatHerdrError(detected.error), true);
|
|
88
|
+
export async function openHerdrInspector(
|
|
89
|
+
context: InspectorContext,
|
|
90
|
+
launch: InspectorLaunch,
|
|
91
|
+
params: InspectorParams,
|
|
92
|
+
client: HerdrClient,
|
|
93
|
+
): Promise<AgentToolResult<Details>> {
|
|
94
|
+
const detected = await detectHerdr(client, context.signal);
|
|
95
|
+
if (!detected.ok) return result(errorText(detected.error), true);
|
|
96
|
+
const existing = readHerdrInspectorBindingForTarget(context.target);
|
|
192
97
|
if (existing) {
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
98
|
+
const current = await pane(client, existing.paneId, context.signal);
|
|
99
|
+
if (current.ok && paneId(current.data) === existing.paneId) {
|
|
100
|
+
return result(`Herdr inspector pane ${existing.paneId} is already open for async run ${context.target.runId}.${params.focus ? " Herdr cannot refocus an arbitrary raw pane id; select it in the Herdr UI." : ""}`);
|
|
101
|
+
}
|
|
195
102
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
index: params.index,
|
|
209
|
-
missionPath: mission?.path,
|
|
210
|
-
allowSteer: resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto",
|
|
211
|
-
allowStop: resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto",
|
|
212
|
-
sessionRoots: herdrSessionRoots(target, deps),
|
|
213
|
-
});
|
|
214
|
-
const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15_000, signal: deps.signal });
|
|
215
|
-
if (started.ok === false) {
|
|
216
|
-
await client.run(["pane", "close", paneId], { timeoutMs: 5_000 });
|
|
217
|
-
return result(formatHerdrError(started.error), true);
|
|
103
|
+
const split = await client.run([
|
|
104
|
+
"pane", "split", "--current", "--direction", "right",
|
|
105
|
+
"--cwd", context.target.status.cwd ?? context.cwd,
|
|
106
|
+
params.focus === true ? "--focus" : "--no-focus",
|
|
107
|
+
], { timeoutMs: 15_000, signal: context.signal });
|
|
108
|
+
if (!split.ok) return result(errorText(split.error), true);
|
|
109
|
+
const id = paneId(split.data);
|
|
110
|
+
if (!id) return result("Herdr inspector error (PANE_GONE): pane split returned no pane id.", true);
|
|
111
|
+
const started = await client.run(["pane", "run", id, launch.displayCommand], { timeoutMs: 15_000, signal: context.signal });
|
|
112
|
+
if (!started.ok) {
|
|
113
|
+
await client.run(["pane", "close", id], { timeoutMs: 5_000 });
|
|
114
|
+
return result(errorText(started.error), true);
|
|
218
115
|
}
|
|
219
|
-
const now = (
|
|
116
|
+
const now = (context.now?.() ?? new Date()).toISOString();
|
|
220
117
|
const binding: HerdrInspectorBinding = {
|
|
221
118
|
schemaVersion: 1,
|
|
222
119
|
kind: "herdr-inspector",
|
|
223
|
-
runId: target.runId,
|
|
224
|
-
asyncDir: target.asyncDir,
|
|
225
|
-
...(
|
|
226
|
-
...(mission ? { missionId: mission.id, missionPath: mission.path } : {}),
|
|
227
|
-
paneId,
|
|
120
|
+
runId: context.target.runId,
|
|
121
|
+
asyncDir: context.target.asyncDir,
|
|
122
|
+
...(context.target.index === undefined ? {} : { childIndex: context.target.index }),
|
|
123
|
+
...(launch.mission ? { missionId: launch.mission.id, missionPath: launch.mission.path } : {}),
|
|
124
|
+
paneId: id,
|
|
228
125
|
openedAt: now,
|
|
229
126
|
...(params.focus === true ? { lastFocusedAt: now } : {}),
|
|
230
127
|
herdrVersion: detected.data.versionText,
|
|
231
|
-
command,
|
|
128
|
+
command: launch.displayCommand,
|
|
232
129
|
};
|
|
233
|
-
writeAtomicJson(bindingPath(target.asyncDir,
|
|
234
|
-
return result(`Opened read-only Herdr inspector pane ${
|
|
130
|
+
writeAtomicJson(bindingPath(context.target.asyncDir, context.target.index), binding);
|
|
131
|
+
return result(`Opened read-only Herdr inspector pane ${id} for async run ${context.target.runId}. Closing the pane does not stop the run.\nControls inside the pane: steer <message>, stop, status.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function statusHerdrInspector(context: InspectorContext, client: HerdrClient): Promise<AgentToolResult<Details>> {
|
|
135
|
+
const binding = readHerdrInspectorBindingForTarget(context.target);
|
|
136
|
+
if (!binding) {
|
|
137
|
+
return result(`No Herdr inspector binding exists for async run ${context.target.runId}${context.target.index === undefined ? "" : ` child ${context.target.index}`}.`);
|
|
138
|
+
}
|
|
139
|
+
const live = await pane(client, binding.paneId, context.signal);
|
|
140
|
+
if (!live.ok) {
|
|
141
|
+
return result(`${errorText(live.error)}\nBinding: ${bindingPath(context.target.asyncDir, context.target.index)}\nRun state remains authoritative: ${context.target.status.state}.`, true);
|
|
142
|
+
}
|
|
143
|
+
return result(`Herdr inspector ${binding.paneId} is open for async run ${context.target.runId}.\nRun state: ${context.target.status.state}\nBinding: ${bindingPath(context.target.asyncDir, context.target.index)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function closeHerdrInspector(context: InspectorContext, client: HerdrClient): Promise<AgentToolResult<Details>> {
|
|
147
|
+
const binding = readHerdrInspectorBindingForTarget(context.target);
|
|
148
|
+
if (!binding) return result(`No Herdr inspector binding exists for async run ${context.target.runId}.`);
|
|
149
|
+
const closed = await client.run(["pane", "close", binding.paneId], { timeoutMs: 10_000, signal: context.signal });
|
|
150
|
+
if (!closed.ok && closed.error.code !== "NOT_FOUND" && closed.error.code !== "PANE_GONE") {
|
|
151
|
+
return result(errorText(closed.error), true);
|
|
152
|
+
}
|
|
153
|
+
fs.rmSync(bindingPath(context.target.asyncDir, context.target.index), { force: true });
|
|
154
|
+
return result(`Closed Herdr inspector pane ${binding.paneId} for async run ${context.target.runId}. The subagent run was not stopped.`);
|
|
235
155
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHerdrClient, type HerdrClient } from "./client.ts";
|
|
2
|
+
import { closeHerdrInspector, openHerdrInspector, readHerdrInspectorBindingForTarget, statusHerdrInspector } from "./actions.ts";
|
|
3
|
+
import type { InspectorPlugin } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
export interface HerdrPluginDeps {
|
|
6
|
+
client?: HerdrClient;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createHerdrInspectorPlugin(deps: HerdrPluginDeps = {}): InspectorPlugin {
|
|
10
|
+
const client = deps.client ?? createHerdrClient();
|
|
11
|
+
return {
|
|
12
|
+
name: "herdr",
|
|
13
|
+
available: async (context) => context.env.HERDR_ENV === "1"
|
|
14
|
+
&& Boolean(context.env.HERDR_PANE_ID?.trim()),
|
|
15
|
+
owns: (context) => readHerdrInspectorBindingForTarget(context.target) !== undefined,
|
|
16
|
+
open: (context, launch, params) => openHerdrInspector(context, launch, params, client),
|
|
17
|
+
status: (context) => statusHerdrInspector(context, client),
|
|
18
|
+
close: (context) => closeHerdrInspector(context, client),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -7,7 +7,7 @@ import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
|
7
7
|
import type { Details, HerdrProjectPaneSnapshot, SubagentState } from "../../shared/types.ts";
|
|
8
8
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode } from "./client.ts";
|
|
9
9
|
import { focusHerdrPane, herdrPaneFocusTarget, herdrPaneRecord } from "./focus.ts";
|
|
10
|
-
import { formatShellCommand } from "
|
|
10
|
+
import { formatShellCommand } from "../shell-command.ts";
|
|
11
11
|
|
|
12
12
|
export const HERDR_PROJECT_PANE_ACTIONS = ["project.open", "project.status", "project.close"] as const;
|
|
13
13
|
export type HerdrProjectPaneAction = typeof HERDR_PROJECT_PANE_ACTIONS[number];
|
|
@@ -2,13 +2,13 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { parseMissionRecord } from "
|
|
6
|
-
import type { MissionRecord } from "
|
|
7
|
-
import { requestAsyncSteer, requestAsyncStop } from "
|
|
8
|
-
import { formatAsyncRunTranscript } from "
|
|
9
|
-
import { steeringReceipt } from "
|
|
10
|
-
import type { AsyncStatus } from "
|
|
11
|
-
import { readStatus } from "
|
|
5
|
+
import { parseMissionRecord } from "../missions/store.ts";
|
|
6
|
+
import type { MissionRecord } from "../missions/types.ts";
|
|
7
|
+
import { requestAsyncSteer, requestAsyncStop } from "../runs/background/control-channel.ts";
|
|
8
|
+
import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
|
|
9
|
+
import { steeringReceipt } from "../runs/background/steering.ts";
|
|
10
|
+
import type { AsyncStatus } from "../shared/types.ts";
|
|
11
|
+
import { readStatus } from "../shared/utils.ts";
|
|
12
12
|
import { decodeSessionRoots } from "./session-roots-codec.ts";
|
|
13
13
|
|
|
14
14
|
export interface RunnerOptions {
|
|
@@ -31,7 +31,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
|
|
|
31
31
|
const { status, asyncDir, mission } = input;
|
|
32
32
|
const lines = [
|
|
33
33
|
`pi-subagents inspector for ${status.runId}`,
|
|
34
|
-
"This
|
|
34
|
+
"This inspector mirrors lifecycle artifacts; closing it does not stop the run.",
|
|
35
35
|
"",
|
|
36
36
|
];
|
|
37
37
|
if (mission) {
|
|
@@ -43,7 +43,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
|
|
|
43
43
|
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
|
|
44
44
|
const acceptsPlainGuidance = input.index !== undefined || status.mode === "single";
|
|
45
45
|
const controls = [input.allowSteer === false || !acceptsPlainGuidance ? undefined : "type guidance", input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
|
|
46
|
-
lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
|
|
46
|
+
lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom); this inspector is read-only.");
|
|
47
47
|
return lines.join("\n");
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -91,7 +91,7 @@ function queueInspectorSteer(options: RunnerOptions, status: AsyncStatus, messag
|
|
|
91
91
|
requestAsyncSteer(options.asyncDir, {
|
|
92
92
|
message,
|
|
93
93
|
...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
|
|
94
|
-
source: "
|
|
94
|
+
source: "inspector-runner",
|
|
95
95
|
});
|
|
96
96
|
return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
|
|
97
97
|
}
|
|
@@ -104,7 +104,7 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
|
|
|
104
104
|
if (command === "stop") {
|
|
105
105
|
if (options.allowStop === false) throw new Error("Authority policy does not allow stop from this inspector.");
|
|
106
106
|
if (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be stopped.`);
|
|
107
|
-
requestAsyncStop(options.asyncDir, { source: "
|
|
107
|
+
requestAsyncStop(options.asyncDir, { source: "inspector-runner" });
|
|
108
108
|
return `Stop requested for run ${options.runId}.`;
|
|
109
109
|
}
|
|
110
110
|
if (command.startsWith("steer ")) {
|
|
@@ -147,7 +147,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
|
|
|
147
147
|
|
|
148
148
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
149
149
|
try { runInspector(); } catch (cause) {
|
|
150
|
-
process.stderr.write(`
|
|
150
|
+
process.stderr.write(`Inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
151
151
|
process.exitCode = 1;
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createHerdrInspectorPlugin } from "./herdr/plugin.ts";
|
|
2
|
+
import { createGhosttyInspectorPlugin } from "./ghostty/plugin.ts";
|
|
3
|
+
import type { InspectorPlugin } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/** Built-in inspector plugins, ordered by host preference. */
|
|
6
|
+
export function createBuiltinInspectorPlugins(): readonly InspectorPlugin[] {
|
|
7
|
+
return [createHerdrInspectorPlugin(), createGhosttyInspectorPlugin()];
|
|
8
|
+
}
|
|
@@ -19,24 +19,13 @@ function parseStringArray(value: unknown): string[] | undefined {
|
|
|
19
19
|
return value;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Decodes a `--session-roots` argument produced by {@link encodeSessionRoots}.
|
|
24
|
-
* Falls back to parsing the value as raw JSON so any externally-launched
|
|
25
|
-
* inspector runner (a cached copy, or a manual invocation) that still passes
|
|
26
|
-
* the legacy unencoded form keeps working.
|
|
27
|
-
*/
|
|
22
|
+
/** Decodes a `--session-roots` argument produced by {@link encodeSessionRoots}. */
|
|
28
23
|
export function decodeSessionRoots(raw: string): string[] {
|
|
29
24
|
try {
|
|
30
25
|
const decoded = parseStringArray(JSON.parse(Buffer.from(raw, "base64").toString("utf-8")));
|
|
31
26
|
if (decoded) return decoded;
|
|
32
27
|
} catch {
|
|
33
|
-
//
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const parsed = parseStringArray(JSON.parse(raw));
|
|
37
|
-
if (parsed) return parsed;
|
|
38
|
-
} catch {
|
|
39
|
-
// fall through to the shared error below
|
|
28
|
+
// Report one stable validation error below.
|
|
40
29
|
}
|
|
41
|
-
throw new Error("--session-roots must be a base64-encoded
|
|
30
|
+
throw new Error("--session-roots must be a base64-encoded JSON array of strings.");
|
|
42
31
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Details } from "../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
export const INSPECTOR_ACTIONS = ["inspector.open", "inspector.command", "inspector.status", "inspector.close"] as const;
|
|
5
|
+
export type InspectorAction = typeof INSPECTOR_ACTIONS[number];
|
|
6
|
+
|
|
7
|
+
export interface InspectorParams {
|
|
8
|
+
id?: string;
|
|
9
|
+
runId?: string;
|
|
10
|
+
dir?: string;
|
|
11
|
+
index?: number;
|
|
12
|
+
focus?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface InspectorTarget {
|
|
16
|
+
runId: string;
|
|
17
|
+
asyncDir: string;
|
|
18
|
+
index?: number;
|
|
19
|
+
status: {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
state: string;
|
|
22
|
+
steps?: unknown[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface InspectorContext {
|
|
27
|
+
cwd: string;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
target: InspectorTarget;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InspectorLaunch {
|
|
35
|
+
executable: string;
|
|
36
|
+
argv: string[];
|
|
37
|
+
displayCommand: string;
|
|
38
|
+
mission?: { id: string; path: string };
|
|
39
|
+
allowSteer: boolean;
|
|
40
|
+
allowStop: boolean;
|
|
41
|
+
sessionRoots: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface InspectorPlugin {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
available(context: InspectorContext): Promise<boolean> | boolean;
|
|
47
|
+
owns(context: InspectorContext): boolean;
|
|
48
|
+
open(context: InspectorContext, launch: InspectorLaunch, params: InspectorParams): Promise<AgentToolResult<Details>>;
|
|
49
|
+
status?(context: InspectorContext): Promise<AgentToolResult<Details>>;
|
|
50
|
+
close?(context: InspectorContext): Promise<AgentToolResult<Details>>;
|
|
51
|
+
}
|