pi-subagents 0.65.1 → 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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -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 +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -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 +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- 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 +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- 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 +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -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/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- 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-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- 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 +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- 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 +27 -19
- package/src/tui/render.ts +172 -33
- 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/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
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
|
+
import { DIRS, type Details, type SubagentState } from "../shared/types.ts";
|
|
10
|
+
import { readStatus } from "../shared/utils.ts";
|
|
11
|
+
import { resolveSubagentRunId } from "../runs/background/run-id-resolver.ts";
|
|
12
|
+
import { resolveNodeExecutable } from "../shared/node-executable.ts";
|
|
13
|
+
import { encodeSessionRoots } from "./session-roots-codec.ts";
|
|
14
|
+
import { formatShellCommand } from "./shell-command.ts";
|
|
15
|
+
import type { InspectorAction, InspectorContext, InspectorLaunch, InspectorParams, InspectorPlugin, InspectorTarget } from "./types.ts";
|
|
16
|
+
|
|
17
|
+
export { INSPECTOR_ACTIONS } from "./types.ts";
|
|
18
|
+
export type { InspectorAction, InspectorParams, InspectorPlugin } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
21
|
+
const response: AgentToolResult<Details> = {
|
|
22
|
+
content: [{ type: "text", text }],
|
|
23
|
+
details: { mode: "management", results: [] },
|
|
24
|
+
};
|
|
25
|
+
if (isError) response.isError = true;
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pathWithin(base: string, candidate: string): boolean {
|
|
30
|
+
const resolvedBase = path.resolve(base);
|
|
31
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
32
|
+
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
33
|
+
}
|
|
34
|
+
function trustedDir(dir: string, deps: InspectorDispatcherDeps): boolean {
|
|
35
|
+
try {
|
|
36
|
+
if (fs.lstatSync(dir).isSymbolicLink() || !fs.statSync(dir).isDirectory()) return false;
|
|
37
|
+
const real = fs.realpathSync(dir);
|
|
38
|
+
if ([...(deps.state?.asyncJobs.values() ?? []), ...(deps.state?.fleetJobs?.values() ?? [])].some((job) => {
|
|
39
|
+
try {
|
|
40
|
+
return fs.realpathSync(job.asyncDir) === real;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
})) return true;
|
|
45
|
+
const root = deps.asyncDirRoot ?? DIRS.async;
|
|
46
|
+
return fs.existsSync(root) && pathWithin(root, dir) && pathWithin(fs.realpathSync(root), real);
|
|
47
|
+
} catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
function resolveTarget(params: InspectorParams, deps: InspectorDispatcherDeps): InspectorTarget | { error: string } {
|
|
50
|
+
const requested = params.id ?? params.runId;
|
|
51
|
+
let runId: string;
|
|
52
|
+
let asyncDir: string;
|
|
53
|
+
if (params.dir) {
|
|
54
|
+
asyncDir = path.resolve(params.dir);
|
|
55
|
+
if (!trustedDir(asyncDir, deps)) return { error: `Async run directory '${asyncDir}' is outside trusted run roots.` };
|
|
56
|
+
const status = readStatus(asyncDir);
|
|
57
|
+
if (!status) return { error: `No async run status found in '${asyncDir}'.` };
|
|
58
|
+
if (requested && requested !== status.runId && !status.runId.startsWith(requested)) return { error: `Run '${requested}' does not match status run '${status.runId}'.` };
|
|
59
|
+
runId = status.runId;
|
|
60
|
+
} else {
|
|
61
|
+
if (!requested) return { error: "Inspector actions require id or dir." };
|
|
62
|
+
try {
|
|
63
|
+
const found = resolveSubagentRunId(requested, { state: deps.state, asyncDirRoot: deps.asyncDirRoot ?? DIRS.async, resultsDir: deps.resultsDir ?? DIRS.results });
|
|
64
|
+
if (!found) return { error: `No subagent run found for '${requested}'.` };
|
|
65
|
+
if (found.kind !== "async" || !found.location.asyncDir) return { error: `Run '${found.id}' is not an inspectable async run with lifecycle artifacts.` };
|
|
66
|
+
runId = found.id;
|
|
67
|
+
asyncDir = found.location.asyncDir;
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
return { error: cause instanceof Error ? cause.message : String(cause) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const status = readStatus(asyncDir);
|
|
73
|
+
if (!status) return { error: `No lifecycle status exists for async run '${runId}'.` };
|
|
74
|
+
if (params.index !== undefined && (params.index < 0 || params.index >= (status.steps?.length ?? 0))) return { error: `Async run '${runId}' has ${status.steps?.length ?? 0} children. Index ${params.index} is out of range.` };
|
|
75
|
+
const target: InspectorTarget = { runId, asyncDir, status: { cwd: status.cwd, state: status.state, steps: status.steps } };
|
|
76
|
+
if (params.index !== undefined) target.index = params.index;
|
|
77
|
+
return target;
|
|
78
|
+
}
|
|
79
|
+
function missionFor(target: InspectorTarget, deps: InspectorDispatcherDeps): { id: string; path: string } | undefined {
|
|
80
|
+
try {
|
|
81
|
+
const binding = readMissionBinding(target.asyncDir);
|
|
82
|
+
if (binding) return { id: binding.missionId, path: missionRecordPath(binding.location, binding.missionId) };
|
|
83
|
+
const location = resolveMissionStoreLocation(deps.missions ? { projectRoot: deps.cwd, config: deps.missions } : { projectRoot: deps.cwd });
|
|
84
|
+
const mission = listMissions(location).records.find((record) => record.runs.some((run) => run.runId === target.runId));
|
|
85
|
+
return mission ? { id: mission.id, path: missionRecordPath(location, mission.id) } : undefined;
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function launchFor(target: InspectorTarget, deps: InspectorDispatcherDeps): InspectorLaunch {
|
|
91
|
+
const mission = missionFor(target, deps);
|
|
92
|
+
const runnerPath = deps.runnerPath ?? fileURLToPath(new URL("../../inspector-runner.mjs", import.meta.url));
|
|
93
|
+
const job = deps.state?.asyncJobs.get(target.runId) ?? deps.state?.fleetJobs?.get(target.runId);
|
|
94
|
+
const sessionRoots = [...new Set([...(deps.sessionRoots ?? deps.state?.trustedSessionRoots ?? []), ...(job?.sessionRoot ? [job.sessionRoot] : [])])];
|
|
95
|
+
const allowSteer = resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto";
|
|
96
|
+
const allowStop = resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto";
|
|
97
|
+
const argv = [runnerPath, "--async-dir", target.asyncDir, "--run-id", target.runId, "--allow-steer", String(allowSteer), "--allow-stop", String(allowStop), "--session-roots", encodeSessionRoots(sessionRoots)];
|
|
98
|
+
if (target.index !== undefined) argv.push("--index", String(target.index));
|
|
99
|
+
if (mission) argv.push("--mission-path", mission.path);
|
|
100
|
+
const executable = resolveNodeExecutable();
|
|
101
|
+
const launch: InspectorLaunch = { executable, argv, displayCommand: formatShellCommand(executable, argv), allowSteer, allowStop, sessionRoots };
|
|
102
|
+
if (mission) launch.mission = mission;
|
|
103
|
+
return launch;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface InspectorDispatcherDeps {
|
|
107
|
+
state?: SubagentState;
|
|
108
|
+
asyncDirRoot?: string;
|
|
109
|
+
resultsDir?: string;
|
|
110
|
+
missions?: MissionStoreConfig;
|
|
111
|
+
authorityPolicy?: AuthorityPolicyConfig;
|
|
112
|
+
sessionRoots?: string[];
|
|
113
|
+
cwd: string;
|
|
114
|
+
signal?: AbortSignal;
|
|
115
|
+
now?: () => Date;
|
|
116
|
+
runnerPath?: string;
|
|
117
|
+
env?: NodeJS.ProcessEnv;
|
|
118
|
+
plugins?: readonly InspectorPlugin[];
|
|
119
|
+
}
|
|
120
|
+
export async function handleInspectorAction(action: InspectorAction, params: InspectorParams, deps: InspectorDispatcherDeps): Promise<AgentToolResult<Details>> {
|
|
121
|
+
const target = resolveTarget(params, deps);
|
|
122
|
+
if ("error" in target) return result(target.error, true);
|
|
123
|
+
const context: InspectorContext = {
|
|
124
|
+
cwd: deps.cwd,
|
|
125
|
+
signal: deps.signal,
|
|
126
|
+
env: deps.env ?? process.env,
|
|
127
|
+
target,
|
|
128
|
+
};
|
|
129
|
+
if (deps.now) context.now = deps.now;
|
|
130
|
+
if (action === "inspector.command") return result(launchFor(target, deps).displayCommand);
|
|
131
|
+
const plugins = deps.plugins ?? [];
|
|
132
|
+
if (action === "inspector.open") {
|
|
133
|
+
for (const plugin of plugins) {
|
|
134
|
+
if (await plugin.available(context)) return plugin.open(context, launchFor(target, deps), params);
|
|
135
|
+
}
|
|
136
|
+
return result("No inspector plugin is available. Start a supported inspector host, or use inspector.command for a standalone command.", true);
|
|
137
|
+
}
|
|
138
|
+
const owner = plugins.find((plugin) => plugin.owns(context));
|
|
139
|
+
if (!owner) return result(`No inspector plugin owns this binding for async run ${target.runId}.`);
|
|
140
|
+
if (action === "inspector.status") {
|
|
141
|
+
return owner.status
|
|
142
|
+
? owner.status(context)
|
|
143
|
+
: result(`Inspector plugin '${owner.name}' does not support status for async run ${target.runId}.`, true);
|
|
144
|
+
}
|
|
145
|
+
return owner.close
|
|
146
|
+
? owner.close(context)
|
|
147
|
+
: result(`Inspector plugin '${owner.name}' does not support close for async run ${target.runId}.`, true);
|
|
148
|
+
}
|
|
@@ -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];
|