pi-subagents 0.81.0 → 0.82.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/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/bundle/agents.js +3 -3
- package/bundle/child-tool-plan.js +2 -2
- package/bundle/{chunk-L5ZHE4L4.js → chunk-3JCDWF4V.js} +4 -4
- package/bundle/{chunk-LSRUTHT7.js → chunk-43SCHIMD.js} +1 -1
- package/bundle/{chunk-VJHOELQP.js → chunk-46XKCPJR.js} +193 -81
- package/bundle/{chunk-G5DKFLLI.js → chunk-4SDU2BZX.js} +15 -10
- package/bundle/{chunk-2ZSNH26H.js → chunk-4Y2ROR7L.js} +7 -7
- package/bundle/{chunk-V2JWDB5U.js → chunk-52UBALJL.js} +2 -2
- package/bundle/{chunk-3DC5KBVP.js → chunk-5TYMSZX3.js} +1 -1
- package/bundle/{chunk-NNLASTQE.js → chunk-6XJB7PIX.js} +20 -6
- package/bundle/{chunk-ZBC44JJ3.js → chunk-CIZQXJ5Q.js} +22 -9
- package/bundle/{chunk-KSWQVGDW.js → chunk-FXUXCX2K.js} +225 -18
- package/bundle/{chunk-563BHAZ5.js → chunk-G4GZD3OJ.js} +35 -26
- package/bundle/{chunk-ARRKBB2B.js → chunk-GJECHSAR.js} +2 -2
- package/bundle/{chunk-V3BIUPLU.js → chunk-IVAE5Z6Q.js} +256 -52
- package/bundle/{chunk-V6MR5TOQ.js → chunk-JYUKW2SY.js} +25 -19
- package/bundle/{chunk-BJN5LUQY.js → chunk-OYF6AFDY.js} +1 -1
- package/bundle/{chunk-CEKS3TFO.js → chunk-PBK2I7WY.js} +6 -6
- package/bundle/{chunk-TUWLXTAM.js → chunk-PDZWJCHD.js} +681 -320
- package/bundle/{chunk-EYNJ6VL6.js → chunk-PLCCBLPY.js} +2 -2
- package/bundle/{chunk-XMFLCKMQ.js → chunk-QE4AGIMG.js} +339 -132
- package/bundle/{chunk-74RT5YQE.js → chunk-RMQFMMAT.js} +123 -33
- package/bundle/{chunk-WSBNENFS.js → chunk-UCPP4GNU.js} +14 -4
- package/bundle/{chunk-2XYWN7QJ.js → chunk-UTT254JE.js} +2 -2
- package/bundle/{chunk-BPS2AGBZ.js → chunk-VSUSLDNW.js} +93 -52
- package/bundle/{chunk-ZP3HODFH.js → chunk-XSWOTIA3.js} +1 -1
- package/bundle/{chunk-3IF2PGW7.js → chunk-YKEWIB4M.js} +1 -1
- package/bundle/{chunk-26C3WLYQ.js → chunk-ZIV4J4HG.js} +498 -274
- package/bundle/control-channel.js +2 -2
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +2 -2
- package/bundle/preflight.js +8 -8
- package/bundle/project-panes.js +3 -3
- package/docs/agents.md +37 -7
- package/docs/configuration.md +9 -1
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +2 -2
- package/skills/council-mode/SKILL.md +1 -4
- package/skills/pi-subagents/SKILL.md +130 -107
- package/skills/pi-subagents/references/orchestration.md +124 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +96 -295
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +2 -0
- package/src/agents/frontmatter.js +15 -1
- package/src/agents/skills.js +2 -1
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +82 -2
- package/src/extension/reply-workflow-script.js +5 -2
- package/src/extension/schemas.js +3 -3
- package/src/extension/subagent-guide.js +1 -2
- package/src/intercom/native-supervisor-channel.js +20 -0
- package/src/missions/lifecycle.js +7 -1
- package/src/runs/background/active-async-capacity.d.ts +1 -0
- package/src/runs/background/active-async-capacity.js +9 -3
- package/src/runs/background/async-execution.js +20 -10
- package/src/runs/background/async-job-tracker.d.ts +6 -1
- package/src/runs/background/async-job-tracker.js +9 -4
- package/src/runs/background/async-status.d.ts +4 -0
- package/src/runs/background/async-status.js +22 -3
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +1 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +52 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +2 -0
- package/src/runs/background/terminal-run-index.js +44 -3
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +6 -3
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +45 -6
- package/src/tui/fleet.d.ts +1 -0
- package/src/tui/fleet.js +25 -15
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- package/src/workflows/workflow-stop-cause.js +8 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +0 -74
- package/skills/pi-subagents/references/execution-controls.md +0 -563
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -165
- package/skills/pi-subagents/references/multi-lane-orchestration.md +0 -61
- package/skills/pi-subagents/references/review-and-validation.md +0 -73
|
@@ -2,7 +2,7 @@ import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { ChildRuntimeConfig } from "./child-runtime-config.ts";
|
|
4
4
|
import type { RequiredChildExtensionSnapshot } from "../../shared/required-child-extensions.ts";
|
|
5
|
-
import type {
|
|
5
|
+
import type { PlacementMachineReference, HerdrRemoteGitStatus } from "../../shared/types.ts";
|
|
6
6
|
export interface ChildSessionEvent {
|
|
7
7
|
type: string;
|
|
8
8
|
[key: string]: unknown;
|
|
@@ -32,7 +32,7 @@ export type ChildSessionStorage = {
|
|
|
32
32
|
export interface ChildSessionLaunch {
|
|
33
33
|
cwd: string;
|
|
34
34
|
/** Resolved pane-native placement. Local launches omit this field. */
|
|
35
|
-
machine?:
|
|
35
|
+
machine?: PlacementMachineReference;
|
|
36
36
|
/** The launching session's project trust; undefined keeps Pi's default for hosts without trust. */
|
|
37
37
|
projectTrusted?: boolean;
|
|
38
38
|
/** Process-local provider source owned by the invoking foreground parent. */
|
|
@@ -107,6 +107,7 @@ export interface ChildSession {
|
|
|
107
107
|
readonly virtualModelId?: string;
|
|
108
108
|
readonly contextWindow?: number;
|
|
109
109
|
readonly machineEvidence?: {
|
|
110
|
+
provider: PlacementMachineReference["provider"];
|
|
110
111
|
machineId: string;
|
|
111
112
|
initial?: HerdrRemoteGitStatus;
|
|
112
113
|
final?: HerdrRemoteGitStatus;
|
|
@@ -76,6 +76,34 @@ function inheritParentProviders(modelRuntime, parentProviders, claimedProviderId
|
|
|
76
76
|
}
|
|
77
77
|
const CHILD_PROMPT_RUNTIME_EXTENSION_PATH = "<inline:pi-subagents:prompt-runtime>";
|
|
78
78
|
const CHILD_PROMPT_BOUNDARY_EXTENSION_PATH = "<inline:pi-subagents:prompt-boundary>";
|
|
79
|
+
/** Same class as UNICODE_SPACES in pi-coding-agent dist/utils/paths.js, which resolvePath(..., { normalizeUnicodeSpaces: true }) replaces with a space. */
|
|
80
|
+
const PI_UNICODE_SPACES = new RegExp("[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]", "g");
|
|
81
|
+
function childExtensionFileIdentity(extensionPath) {
|
|
82
|
+
if (!path.isAbsolute(extensionPath))
|
|
83
|
+
return undefined;
|
|
84
|
+
try {
|
|
85
|
+
const stat = fs.statSync(extensionPath, { bigint: true });
|
|
86
|
+
return stat.isFile() ? `${stat.dev}:${stat.ino}` : undefined;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function createRequiredChildExtensionIdentity(requiredPaths) {
|
|
93
|
+
const aliases = new Map([...requiredPaths].map((extensionPath) => [extensionPath, childExtensionFileIdentity(extensionPath) ?? extensionPath]));
|
|
94
|
+
const identities = new Set(aliases.values());
|
|
95
|
+
return (extensionPath) => {
|
|
96
|
+
const known = aliases.get(extensionPath);
|
|
97
|
+
if (known !== undefined)
|
|
98
|
+
return known;
|
|
99
|
+
const identity = childExtensionFileIdentity(extensionPath);
|
|
100
|
+
if (identity === undefined || !identities.has(identity))
|
|
101
|
+
return undefined;
|
|
102
|
+
// Retain the alias even if the extension removes its file before reporting a failure.
|
|
103
|
+
aliases.set(extensionPath, identity);
|
|
104
|
+
return identity;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
79
107
|
/** The prompt runtime filters parent-only context before ambient extensions inspect
|
|
80
108
|
* the child prompt, and the boundary hook appends the child boundary after every
|
|
81
109
|
* extension has added its prompt sections. Every other extension keeps its order. */
|
|
@@ -229,7 +257,7 @@ function missingBuiltinMcpTools(session, names) {
|
|
|
229
257
|
const direct = new Set(session.getAllTools().filter((tool) => tool.exposure === "direct").map(({ name }) => name));
|
|
230
258
|
return names.filter((name) => !active.has(name) || !direct.has(name));
|
|
231
259
|
}
|
|
232
|
-
function flushQueuedProviderRegistrations(loader, modelRuntime, onError,
|
|
260
|
+
function flushQueuedProviderRegistrations(loader, modelRuntime, onError, isRequiredSource) {
|
|
233
261
|
const claimedProviderIds = new Set();
|
|
234
262
|
if (!("getExtensions" in loader) || typeof loader.getExtensions !== "function")
|
|
235
263
|
return { claimedProviderIds, registered: false };
|
|
@@ -243,7 +271,7 @@ function flushQueuedProviderRegistrations(loader, modelRuntime, onError, require
|
|
|
243
271
|
}
|
|
244
272
|
catch (error) {
|
|
245
273
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
246
|
-
if (
|
|
274
|
+
if (isRequiredSource(extensionPath))
|
|
247
275
|
throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
248
276
|
}
|
|
249
277
|
}
|
|
@@ -257,7 +285,7 @@ function flushQueuedProviderRegistrations(loader, modelRuntime, onError, require
|
|
|
257
285
|
}
|
|
258
286
|
catch (error) {
|
|
259
287
|
onError?.({ extensionPath, event: "register_provider", error });
|
|
260
|
-
if (
|
|
288
|
+
if (isRequiredSource(extensionPath))
|
|
261
289
|
throw new Error(`Required child extension provider registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
262
290
|
}
|
|
263
291
|
}
|
|
@@ -280,7 +308,7 @@ function flushQueuedProviderRegistrations(loader, modelRuntime, onError, require
|
|
|
280
308
|
}
|
|
281
309
|
catch (error) {
|
|
282
310
|
onError?.({ extensionPath, event: "register_virtual_model", error });
|
|
283
|
-
if (
|
|
311
|
+
if (isRequiredSource(extensionPath))
|
|
284
312
|
throw new Error(`Required child extension virtual model registration failed for '${extensionPath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
285
313
|
}
|
|
286
314
|
}
|
|
@@ -394,6 +422,13 @@ export function createDefaultChildSessionFactory(options = {}) {
|
|
|
394
422
|
},
|
|
395
423
|
})) }]
|
|
396
424
|
: [];
|
|
425
|
+
const requiredPaths = new Set((launch.requiredExtensions ?? []).map(({ path }) => path));
|
|
426
|
+
const requiredExtensionIdentity = createRequiredChildExtensionIdentity(requiredPaths);
|
|
427
|
+
const resolvedExtensionPaths = new Map();
|
|
428
|
+
// Pi reports failures by source path, without resolvedPath. A source Pi never loaded is classified by the
|
|
429
|
+
// file it names, after Pi's Unicode-space normalization. A required extension that failed under an
|
|
430
|
+
// unmatched spelling is still rejected by the missing-identity check after loading.
|
|
431
|
+
const isRequiredSource = (extensionPath) => requiredPaths.size > 0 && requiredExtensionIdentity(resolvedExtensionPaths.get(extensionPath) ?? extensionPath.replace(PI_UNICODE_SPACES, " ")) !== undefined;
|
|
397
432
|
const loader = new pi.DefaultResourceLoader({
|
|
398
433
|
cwd: launch.cwd,
|
|
399
434
|
agentDir,
|
|
@@ -435,16 +470,24 @@ export function createDefaultChildSessionFactory(options = {}) {
|
|
|
435
470
|
};
|
|
436
471
|
let inheritedProviders = new Set();
|
|
437
472
|
const open = async () => {
|
|
438
|
-
const requiredPaths = new Set((launch.requiredExtensions ?? []).map(({ path }) => path));
|
|
439
473
|
applyProcessEnv(launch.processEnv);
|
|
440
474
|
if (!resetExtensionCacheOnReload(loader) && (launch.ambientExtensions || launch.extensionPaths.length))
|
|
441
475
|
launch.onExtensionError?.({ extensionPath: "<loader>", event: "load", error: new Error("pi's extension cache reset is unavailable; extensions loaded into this child share module state with other sessions in this process.") });
|
|
442
476
|
await loader.reload();
|
|
477
|
+
if (requiredPaths.size > 0)
|
|
478
|
+
for (const extension of loader.getExtensions().extensions)
|
|
479
|
+
resolvedExtensionPaths.set(extension.path, extension.resolvedPath);
|
|
443
480
|
const loadErrors = requiredPaths.size > 0
|
|
444
|
-
? loader.getExtensions().errors.filter(({ path }) =>
|
|
481
|
+
? loader.getExtensions().errors.filter(({ path }) => isRequiredSource(path)) : [];
|
|
445
482
|
if (loadErrors.length > 0)
|
|
446
483
|
throw new Error(`Required child extension failed to load: ${loadErrors.map(({ path, error }) => `${path}: ${error}`).join("; ")}`);
|
|
447
|
-
const queued = flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError,
|
|
484
|
+
const queued = flushQueuedProviderRegistrations(loader, modelRuntime, launch.onExtensionError, isRequiredSource);
|
|
485
|
+
if (requiredPaths.size > 0) {
|
|
486
|
+
const loadedIdentities = new Set(loader.getExtensions().extensions.map(({ resolvedPath }) => requiredExtensionIdentity(resolvedPath)));
|
|
487
|
+
const missing = [...requiredPaths].filter((extensionPath) => !loadedIdentities.has(requiredExtensionIdentity(extensionPath)));
|
|
488
|
+
if (missing.length)
|
|
489
|
+
throw new Error(`Required child extension failed to load: ${missing.join(", ")}`);
|
|
490
|
+
}
|
|
448
491
|
if (launch.parentProviderRegistry)
|
|
449
492
|
inheritedProviders = inheritParentProviders(modelRuntime, launch.parentProviderRegistry, queued.claimedProviderIds, launch.onExtensionError);
|
|
450
493
|
if (queued.registered || inheritedProviders.size > 0) {
|
|
@@ -496,7 +539,7 @@ export function createDefaultChildSessionFactory(options = {}) {
|
|
|
496
539
|
await session.bindExtensions({
|
|
497
540
|
mode: "print",
|
|
498
541
|
onError: (error) => {
|
|
499
|
-
if (binding &&
|
|
542
|
+
if (binding && isRequiredSource(error.extensionPath))
|
|
500
543
|
requiredStartupErrors.push(`${error.extensionPath} (${error.event}): ${error.error}`);
|
|
501
544
|
launch.onExtensionError?.({ extensionPath: error.extensionPath, event: error.event, error: error.error });
|
|
502
545
|
},
|
|
@@ -65,6 +65,13 @@ export interface PiLaunchToolPlan {
|
|
|
65
65
|
* runtime snapshot therefore cannot work and fails the launch.
|
|
66
66
|
*/
|
|
67
67
|
export declare function formatRuntimeSnapshotMcpServersError(agentName: string | undefined, serverNames: readonly string[]): string;
|
|
68
|
+
/**
|
|
69
|
+
* A child's `tools` list is a complete allowlist. Read as names, Pi's modifier
|
|
70
|
+
* entries would become required tools no child has, or mix with the names the
|
|
71
|
+
* plan adds into a list Pi rejects. The modifier rule is Pi's `isToolModifier`:
|
|
72
|
+
* a `+name` or `-name` entry edits Pi's default tool selection.
|
|
73
|
+
*/
|
|
74
|
+
export declare function assertNoToolModifierEntries(agentName: string | undefined, tools: readonly string[]): void;
|
|
68
75
|
export declare function projectLaunchResolvedChildExtensions(toolPlan: Pick<PiLaunchToolPlan, "runtimeExtensions" | "configuredExtensions" | "requiredExtensions" | "extensionArgs" | "disableAmbientExtensions">): LaunchResolvedChildExtensions;
|
|
69
76
|
/**
|
|
70
77
|
* Resolve the permission-system extension entry point when installed.
|
|
@@ -100,6 +100,23 @@ export function formatRuntimeSnapshotMcpServersError(agentName, serverNames) {
|
|
|
100
100
|
const subject = agentName ? `Agent '${agentName}'` : "Subagent";
|
|
101
101
|
return `${subject} selects MCP tools from servers that exist only in pi-mcp-adapter's runtime snapshot (${serverNames.join(", ")}). MCP servers from the runtime snapshot cannot be provided to in-process children; MCP tools must come from an ambient adapter extension in a background child (\`async: true\`), so add the server to the adapter's configuration file instead.`;
|
|
102
102
|
}
|
|
103
|
+
/** Path-like entries load extensions instead of naming tools, so Pi never reads them as modifiers. */
|
|
104
|
+
function isToolExtensionPath(entry) {
|
|
105
|
+
return entry.includes("/") || entry.endsWith(".ts") || entry.endsWith(".js");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A child's `tools` list is a complete allowlist. Read as names, Pi's modifier
|
|
109
|
+
* entries would become required tools no child has, or mix with the names the
|
|
110
|
+
* plan adds into a list Pi rejects. The modifier rule is Pi's `isToolModifier`:
|
|
111
|
+
* a `+name` or `-name` entry edits Pi's default tool selection.
|
|
112
|
+
*/
|
|
113
|
+
export function assertNoToolModifierEntries(agentName, tools) {
|
|
114
|
+
const entries = tools.filter((entry) => /^[+-]/u.test(entry) && !isToolExtensionPath(entry));
|
|
115
|
+
if (entries.length === 0)
|
|
116
|
+
return;
|
|
117
|
+
const subject = agentName ? `Agent '${agentName}'` : "Subagent";
|
|
118
|
+
throw new Error(`${subject} lists Pi tool modifier ${entries.length === 1 ? "entry" : "entries"} in \`tools\`: ${entries.join(", ")}. A child's \`tools\` list is the complete set of tools it gets, so +name and -name entries are not supported; name every tool the child needs, or omit \`tools\` and use \`excludeTools\` to drop one.`);
|
|
119
|
+
}
|
|
103
120
|
export function projectLaunchResolvedChildExtensions(toolPlan) {
|
|
104
121
|
const runtime = boundedExtensionIdentifiers(toolPlan.runtimeExtensions);
|
|
105
122
|
const configured = boundedExtensionIdentifiers(toolPlan.configuredExtensions);
|
|
@@ -178,7 +195,8 @@ export function resolvePiLaunchToolPlan(input) {
|
|
|
178
195
|
const allowedToolSet = capabilityCeiling?.allowedTools === undefined
|
|
179
196
|
? undefined
|
|
180
197
|
: new Set(capabilityCeiling.allowedTools);
|
|
181
|
-
const requestedBuiltinTools = input.tools?.filter((tool) => !(tool
|
|
198
|
+
const requestedBuiltinTools = input.tools?.filter((tool) => !isToolExtensionPath(tool)) ?? [];
|
|
199
|
+
assertNoToolModifierEntries(input.agentName, input.tools ?? []);
|
|
182
200
|
if (input.requireReadTool && allowedToolSet && !allowedToolSet.has("read")) {
|
|
183
201
|
throw new Error(`Capability ceiling from ${capabilityCeiling?.sources.join(", ") || "unknown source"} excludes required tool 'read' for lazy skill loading.`);
|
|
184
202
|
}
|
|
@@ -204,8 +222,7 @@ export function resolvePiLaunchToolPlan(input) {
|
|
|
204
222
|
}
|
|
205
223
|
const toolExtensionPaths = capabilityCeiling?.denyExtensions
|
|
206
224
|
? []
|
|
207
|
-
: (input.tools ?? []).filter(
|
|
208
|
-
(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")));
|
|
225
|
+
: (input.tools ?? []).filter(isToolExtensionPath);
|
|
209
226
|
const mcpResolution = capabilityCeiling?.denyExtensions
|
|
210
227
|
? { selections: [], unresolvedSelectors: [] }
|
|
211
228
|
: input.builtinMcpTools
|
|
@@ -318,9 +335,7 @@ export function resolvePiLaunchToolPlan(input) {
|
|
|
318
335
|
removedExtensionCount: capabilityCeiling.denyExtensions
|
|
319
336
|
? (input.extensions?.length ?? 0) +
|
|
320
337
|
(input.subagentOnlyExtensions?.length ?? 0) +
|
|
321
|
-
(input.tools ?? []).filter(
|
|
322
|
-
tool.endsWith(".ts") ||
|
|
323
|
-
tool.endsWith(".js")).length
|
|
338
|
+
(input.tools ?? []).filter(isToolExtensionPath).length
|
|
324
339
|
: 0,
|
|
325
340
|
requestedMcpToolCount: input.mcpDirectTools?.length ?? 0,
|
|
326
341
|
effectiveMcpTools,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HerdrMachineReference } from "../../shared/types.ts";
|
|
2
|
+
import type { PlacementCommandResult, PlacementMachineAccess } from "./placement-host.ts";
|
|
2
3
|
export declare const HERDR_REMOTE_PATH = "$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
3
4
|
export declare const HERDR_SSH_BASE: readonly ["-T", "-o", "BatchMode=yes", "-o", "NumberOfPasswordPrompts=0", "-o", "StrictHostKeyChecking=yes", "-o", "ConnectTimeout=10", "-o", "ConnectionAttempts=1", "-o", "ForwardAgent=no", "-o", "ExitOnForwardFailure=yes", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=4", "-o", "SendEnv=-*", "-o", "SetEnv=PI_SUBAGENTS_SSH_GUARD="];
|
|
4
5
|
export declare function hardenedSshEnv(source?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -33,26 +34,28 @@ export interface HerdrForwardedConnection {
|
|
|
33
34
|
}>;
|
|
34
35
|
close(): Promise<void>;
|
|
35
36
|
}
|
|
37
|
+
type SshTarget = Pick<HerdrMachineReference, "target">;
|
|
36
38
|
export declare function decodeHerdrJsonLine(line: Buffer): unknown;
|
|
37
39
|
export declare function shellQuoteRemote(value: string): string;
|
|
38
40
|
export declare function remoteShellCommand(script: string, args?: string[]): string;
|
|
39
|
-
export declare function runHerdrRemoteCommand(machine:
|
|
41
|
+
export declare function runHerdrRemoteCommand(machine: SshTarget, command: string, options?: {
|
|
40
42
|
sshBin?: string;
|
|
41
43
|
env?: NodeJS.ProcessEnv;
|
|
42
44
|
timeout?: number;
|
|
43
45
|
maxBuffer?: number;
|
|
44
46
|
}): import("child_process").SpawnSyncReturns<string>;
|
|
45
|
-
export declare function runHerdrRemoteCommandAsync(machine:
|
|
47
|
+
export declare function runHerdrRemoteCommandAsync(machine: SshTarget, command: string, options?: {
|
|
46
48
|
sshBin?: string;
|
|
47
49
|
env?: NodeJS.ProcessEnv;
|
|
48
50
|
timeout?: number;
|
|
49
51
|
maxBuffer?: number;
|
|
50
|
-
}): Promise<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
}): Promise<PlacementCommandResult>;
|
|
53
|
+
/** Spawns one process with bounded output and a SIGTERM-then-SIGKILL timeout; never rejects. */
|
|
54
|
+
export declare function runBoundedCommand(file: string, args: string[], options: {
|
|
55
|
+
env: NodeJS.ProcessEnv;
|
|
56
|
+
timeout?: number;
|
|
57
|
+
maxBuffer?: number;
|
|
58
|
+
}): Promise<PlacementCommandResult>;
|
|
56
59
|
export declare function parseHerdrEndpoint(value: string, expectedSession?: string): HerdrEndpoint;
|
|
57
60
|
export declare function discoverHerdrEndpoint(machine: HerdrMachineReference, options?: {
|
|
58
61
|
sshBin?: string;
|
|
@@ -64,7 +67,13 @@ export declare class SocketRpcClient implements HerdrRpcClient {
|
|
|
64
67
|
call<T>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
65
68
|
subscribe(subscriptions: unknown[], listener: (event: unknown) => void, onDisconnect?: (error: Error) => void): Promise<() => void>;
|
|
66
69
|
}
|
|
70
|
+
/** SSH machine access: hardened ssh commands and StreamLocal forwards that share one private local directory per socket scope. */
|
|
71
|
+
export declare function sshMachineAccess(machine: SshTarget, options?: {
|
|
72
|
+
sshBin?: string;
|
|
73
|
+
env?: NodeJS.ProcessEnv;
|
|
74
|
+
}): PlacementMachineAccess;
|
|
67
75
|
export declare function connectHerdrMachine(machine: HerdrMachineReference, options?: {
|
|
68
76
|
sshBin?: string;
|
|
69
77
|
env?: NodeJS.ProcessEnv;
|
|
70
78
|
}): Promise<HerdrForwardedConnection>;
|
|
79
|
+
export {};
|
|
@@ -30,8 +30,12 @@ function canonicalHerdrSession(session) { return session && session !== "default
|
|
|
30
30
|
function sshEnvCommand(session, command) { const selected = canonicalHerdrSession(session), assignment = selected ? ` HERDR_SESSION=${shellQuoteRemote(selected)}` : ""; return `/usr/bin/env -u HERDR_SOCKET_PATH -u HERDR_SESSION${assignment} ${remoteShellCommand(command)}`; }
|
|
31
31
|
export function runHerdrRemoteCommand(machine, command, options = {}) { return spawnSync(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), machine.target, command], { encoding: "utf8", env: hardenedSshEnv(options.env), timeout: options.timeout ?? 15_000, maxBuffer: options.maxBuffer ?? MAX_DISCOVERY_BYTES, windowsHide: true }); }
|
|
32
32
|
export function runHerdrRemoteCommandAsync(machine, command, options = {}) {
|
|
33
|
+
return runBoundedCommand(options.sshBin ?? "ssh", [...herdrSshArgs(options.env), machine.target, command], { env: hardenedSshEnv(options.env), timeout: options.timeout, maxBuffer: options.maxBuffer });
|
|
34
|
+
}
|
|
35
|
+
/** Spawns one process with bounded output and a SIGTERM-then-SIGKILL timeout; never rejects. */
|
|
36
|
+
export function runBoundedCommand(file, args, options) {
|
|
33
37
|
return new Promise((resolve) => {
|
|
34
|
-
const child = spawn(
|
|
38
|
+
const child = spawn(file, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
35
39
|
const stdout = [], stderr = [], maxBuffer = options.maxBuffer ?? MAX_DISCOVERY_BYTES;
|
|
36
40
|
let stdoutBytes = 0, stderrBytes = 0, error, settled = false, escalation;
|
|
37
41
|
const finish = (status, spawnError = error) => { if (settled)
|
|
@@ -226,10 +230,7 @@ async function terminateChild(child) { const wait = (ms) => child.exitCode !== n
|
|
|
226
230
|
child.kill("SIGKILL");
|
|
227
231
|
await wait(1_000);
|
|
228
232
|
} }
|
|
229
|
-
|
|
230
|
-
if (process.platform === "win32")
|
|
231
|
-
throw new Error("Pane-native Herdr placement requires OpenSSH StreamLocal forwarding and is not supported on Windows.");
|
|
232
|
-
const endpoint = await discoverHerdrEndpoint(machine, options);
|
|
233
|
+
function openSshSockets(machine, options) {
|
|
233
234
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-herdr-"));
|
|
234
235
|
fs.chmodSync(dir, 0o700);
|
|
235
236
|
const children = new Set();
|
|
@@ -252,6 +253,17 @@ export async function connectHerdrMachine(machine, options = {}) {
|
|
|
252
253
|
}
|
|
253
254
|
return { socketPath: local, close: stop };
|
|
254
255
|
};
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
return { forwardRemoteSocket: forward, close: async () => { await Promise.all([...children].map(terminateChild)); children.clear(); fs.rmSync(dir, { recursive: true, force: true }); } };
|
|
257
|
+
}
|
|
258
|
+
/** SSH machine access: hardened ssh commands and StreamLocal forwards that share one private local directory per socket scope. */
|
|
259
|
+
export function sshMachineAccess(machine, options = {}) {
|
|
260
|
+
return { shellCommand: remoteShellCommand, run: (command, limits = {}) => runHerdrRemoteCommand(machine, command, { ...options, ...limits }), runAsync: (command, limits = {}) => runHerdrRemoteCommandAsync(machine, command, { ...options, ...limits }), openSockets: () => openSshSockets(machine, options) };
|
|
261
|
+
}
|
|
262
|
+
export async function connectHerdrMachine(machine, options = {}) {
|
|
263
|
+
if (process.platform === "win32")
|
|
264
|
+
throw new Error("Pane-native Herdr placement requires OpenSSH StreamLocal forwarding and is not supported on Windows.");
|
|
265
|
+
const endpoint = await discoverHerdrEndpoint(machine, options);
|
|
266
|
+
const sockets = sshMachineAccess(machine, options).openSockets();
|
|
267
|
+
const primary = await sockets.forwardRemoteSocket(endpoint.socket, "herdr").catch(async (error) => { await sockets.close(); throw error; });
|
|
268
|
+
return { endpoint, socketPath: primary.socketPath, client: new SocketRpcClient(primary.socketPath), forwardRemoteSocket: sockets.forwardRemoteSocket, close: sockets.close };
|
|
257
269
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HerdrMachineReference, HerdrRemoteGitStatus } from "../../shared/types.ts";
|
|
2
2
|
import { connectHerdrMachine, type HerdrForwardedConnection } from "./herdr-connection.ts";
|
|
3
|
-
import { HerdrPlacedRunOwner, type HerdrReconnectCandidate
|
|
3
|
+
import { HerdrPlacedRunOwner, type HerdrReconnectCandidate } from "./herdr-pane-host.ts";
|
|
4
|
+
import type { HerdrRunIdentity } from "./herdr-placed-run.ts";
|
|
4
5
|
export type HerdrExternalAdapterId = "claude-code" | "claude-code-writer" | "cursor-agent" | "cursor-agent-writer" | "codex-exec" | "codex-exec-writer";
|
|
5
6
|
export type HerdrExternalKind = "claude" | "cursor" | "codex";
|
|
6
7
|
export interface HerdrExternalCapabilities {
|
|
@@ -62,7 +63,7 @@ export interface HerdrExternalAdapter {
|
|
|
62
63
|
}
|
|
63
64
|
export declare function reconcileHerdrExternalRun(identity: HerdrRunIdentity, candidate: HerdrReconnectCandidate): {
|
|
64
65
|
ok: true;
|
|
65
|
-
state: import("./
|
|
66
|
+
state: import("./placement-host.ts").PlacementRunState;
|
|
66
67
|
paneId: string;
|
|
67
68
|
cursor: number;
|
|
68
69
|
} | {
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { stripVTControlCharacters } from "node:util";
|
|
4
4
|
import { connectHerdrMachine, HerdrRpcError, HerdrTransportError, remoteShellCommand, shellQuoteRemote } from "./herdr-connection.js";
|
|
5
|
-
import { HerdrPlacedRunOwner, herdrStatusSubscriptions, ownsHerdrPane, parseHerdrAgent, reconcileHerdrPlacedRun } from "./herdr-
|
|
5
|
+
import { HerdrPlacedRunOwner, herdrStatusSubscriptions, ownsHerdrPane, parseHerdrAgent, reconcileHerdrPlacedRun } from "./herdr-pane-host.js";
|
|
6
6
|
const CAPABILITIES = { stop: true, steer: false, resume: false, supervisor: "unsupported" };
|
|
7
7
|
const MAX_EVIDENCE_BYTES = 4 * 1024 * 1024, MAX_OUTPUT_BYTES = 1024 * 1024, MAX_PREFLIGHT_BYTES = 64 * 1024, MAX_GIT_BYTES = 2048;
|
|
8
8
|
const CODEX_OUTPUT_BYTES = 64 * 1024, EXTERNAL_STARTUP_TIMEOUT_MS = 45_000, EXTERNAL_STARTUP_BACKOFF_AFTER_MS = 15_000, EXTERNAL_STARTUP_SLOW_POLL_MS = 2_500, CODEX_STARTUP_GRACE_MS = 15_000, CODEX_QUIET_MS = 8_000, CODEX_POLL_MS = 1_000, CODEX_DEFAULT_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExternalProcessStatus, HerdrMachineReference } from "../../shared/types.ts";
|
|
1
|
+
import type { ExternalProcessStatus, HerdrMachineReference, PlacementMachineReference } from "../../shared/types.ts";
|
|
2
2
|
import type { runExternalCli } from "./external-cli-runner.ts";
|
|
3
3
|
export { shellQuoteRemote as shellQuote } from "./herdr-connection.ts";
|
|
4
4
|
/** The local ssh process gets only what ssh itself needs; remote runs use the machine's own credentials. */
|
|
@@ -7,6 +7,10 @@ type RunExternalCliInput = Parameters<typeof runExternalCli>[0];
|
|
|
7
7
|
interface MachineSettingsEntry {
|
|
8
8
|
cwd?: string;
|
|
9
9
|
env?: Record<string, string>;
|
|
10
|
+
/** User settings only: run this machine's children in Rex panes reached over SSH at `ssh`. */
|
|
11
|
+
rex?: {
|
|
12
|
+
ssh: string;
|
|
13
|
+
};
|
|
10
14
|
}
|
|
11
15
|
export interface ResolveHerdrMachinePlacementInput {
|
|
12
16
|
/** Profile id or label as typed by the operator. */
|
|
@@ -22,8 +26,12 @@ export interface ResolveHerdrMachinePlacementInput {
|
|
|
22
26
|
settings?: MachineSettingsEntry;
|
|
23
27
|
herdrBin?: string;
|
|
24
28
|
}
|
|
25
|
-
export interface HerdrMachinePlacement {
|
|
29
|
+
export interface HerdrMachinePlacement extends MachinePlacement {
|
|
26
30
|
machine: HerdrMachineReference;
|
|
31
|
+
}
|
|
32
|
+
/** A resolved `machine` value: a Herdr saved machine, or the reserved `local` (a Rex pane on this machine). */
|
|
33
|
+
export interface MachinePlacement {
|
|
34
|
+
machine: PlacementMachineReference;
|
|
27
35
|
/** Opt-in `subagents.machines.<name>.env`, exported in front of the remote command. */
|
|
28
36
|
env?: Record<string, string>;
|
|
29
37
|
}
|
|
@@ -32,6 +40,8 @@ export interface PreparedHerdrMachineExternalCliRun {
|
|
|
32
40
|
decorateProcess(process: ExternalProcessStatus): ExternalProcessStatus;
|
|
33
41
|
}
|
|
34
42
|
export declare function resolveHerdrMachinePlacement(input: ResolveHerdrMachinePlacementInput): HerdrMachinePlacement;
|
|
43
|
+
/** `local` resolves to a Rex pane on this machine; a name with a `rex` entry in user settings to a Rex pane on that SSH host; any other value to a Herdr saved machine. */
|
|
44
|
+
export declare function resolveMachinePlacement(input: ResolveHerdrMachinePlacementInput): MachinePlacement;
|
|
35
45
|
export declare function formatHerdrMachineRunnerUnsupported(input: {
|
|
36
46
|
machine?: string;
|
|
37
47
|
agentName: string;
|
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getAgentDir, getProjectConfigDir } from "../../shared/utils.js";
|
|
5
5
|
import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS } from "./external-cli-contract.js";
|
|
6
|
+
import { REX_LOCAL_MACHINE, resolveRexLocalPlacement } from "./rex-machine.js";
|
|
6
7
|
export { shellQuoteRemote as shellQuote } from "./herdr-connection.js";
|
|
7
8
|
/**
|
|
8
9
|
* Herdr saved-machine placement for external CLI children.
|
|
@@ -149,18 +150,31 @@ function machineSettingsFrom(settings, keys, filePath) {
|
|
|
149
150
|
throw new Error(`Subagent settings in '${filePath}' set 'machines.${key}.env'. Saved-machine runs use the remote Herdr/Pi environment; configure credentials and environment on '${key}' and remove the local env map.`);
|
|
150
151
|
}
|
|
151
152
|
}
|
|
153
|
+
if (record.rex !== undefined) {
|
|
154
|
+
const rex = record.rex, ssh = rex && typeof rex === "object" && !Array.isArray(rex) && Object.keys(rex).every((field) => field === "ssh") ? rex.ssh : undefined;
|
|
155
|
+
if (typeof ssh !== "string")
|
|
156
|
+
throw new Error(`Subagent settings in '${filePath}' have invalid 'machines.${key}.rex'; expected { "ssh": "<ssh target>" }.`);
|
|
157
|
+
entry.rex = { ssh: validateTarget(ssh, key) };
|
|
158
|
+
}
|
|
152
159
|
return entry;
|
|
153
160
|
}
|
|
154
|
-
/** Project settings beat user settings, field by field; the entry may be keyed by label, id, or the typed selector. */
|
|
161
|
+
/** Project settings beat user settings, field by field; the entry may be keyed by label, id, or the typed selector. Only user settings may name a Rex SSH host, so a cloned repo never chooses where a task runs. */
|
|
155
162
|
function readMachineSettings(cwd, keys) {
|
|
156
163
|
const userPath = path.join(getAgentDir(), "settings.json");
|
|
157
164
|
const projectPath = path.join(getProjectConfigDir(findProjectRootForSettings(cwd)), "settings.json");
|
|
158
165
|
const user = machineSettingsFrom(readJsonObject(userPath), keys, userPath);
|
|
159
166
|
const project = projectPath === userPath ? undefined : machineSettingsFrom(readJsonObject(projectPath), keys, projectPath);
|
|
167
|
+
if (project?.rex)
|
|
168
|
+
throw new Error(`Project settings '${projectPath}' set a Rex SSH host in 'machines'. Move the rex entry to your user settings '${userPath}'; only user settings choose which host a task runs on.`);
|
|
160
169
|
if (!user && !project)
|
|
161
170
|
return undefined;
|
|
162
171
|
return { ...user, ...project };
|
|
163
172
|
}
|
|
173
|
+
/** Whether user settings name `machine` as a Rex SSH host. */
|
|
174
|
+
function userRexMachine(machine) {
|
|
175
|
+
const userPath = path.join(getAgentDir(), "settings.json");
|
|
176
|
+
return machineSettingsFrom(readJsonObject(userPath), [machine], userPath)?.rex !== undefined;
|
|
177
|
+
}
|
|
164
178
|
function findProjectRootForSettings(cwd) {
|
|
165
179
|
let current = path.resolve(cwd);
|
|
166
180
|
while (true) {
|
|
@@ -172,6 +186,15 @@ function findProjectRootForSettings(cwd) {
|
|
|
172
186
|
current = parent;
|
|
173
187
|
}
|
|
174
188
|
}
|
|
189
|
+
/** The child's cwd on a remote machine: an absolute launch cwd, or the launch cwd under the machine's configured root. */
|
|
190
|
+
function remoteMachineCwd(stepCwd, root, name) {
|
|
191
|
+
const step = stepCwd?.trim();
|
|
192
|
+
if (step && isRemoteAbsolute(step))
|
|
193
|
+
return step;
|
|
194
|
+
if (!root)
|
|
195
|
+
throw new Error(`No root for ${name} in this repo. Set subagents.machines.${name}.cwd in .pi/settings.json or pass an absolute cwd on that machine.`);
|
|
196
|
+
return step ? path.posix.join(root, step) : root;
|
|
197
|
+
}
|
|
175
198
|
export function resolveHerdrMachinePlacement(input) {
|
|
176
199
|
const requested = validateMachineName(input.machine);
|
|
177
200
|
const catalog = input.catalogJson !== undefined
|
|
@@ -184,14 +207,7 @@ export function resolveHerdrMachinePlacement(input) {
|
|
|
184
207
|
if (settings?.env && Object.keys(settings.env).length > 0) {
|
|
185
208
|
throw new Error(`Saved-machine environment for '${name}' must be configured remotely. Remove machines.${name}.env and configure the remote Herdr/Pi session instead.`);
|
|
186
209
|
}
|
|
187
|
-
const
|
|
188
|
-
let cwd;
|
|
189
|
-
if (stepCwd && isRemoteAbsolute(stepCwd))
|
|
190
|
-
cwd = stepCwd;
|
|
191
|
-
else if (settings?.cwd)
|
|
192
|
-
cwd = stepCwd ? path.posix.join(settings.cwd, stepCwd) : settings.cwd;
|
|
193
|
-
else
|
|
194
|
-
throw new Error(`No root for ${name} in this repo. Set subagents.machines.${name}.cwd in .pi/settings.json or pass an absolute cwd on that machine.`);
|
|
210
|
+
const cwd = remoteMachineCwd(input.stepCwd, settings?.cwd, name);
|
|
195
211
|
return {
|
|
196
212
|
machine: {
|
|
197
213
|
provider: "herdr",
|
|
@@ -204,9 +220,39 @@ export function resolveHerdrMachinePlacement(input) {
|
|
|
204
220
|
...(settings?.env ? { env: settings.env } : {}),
|
|
205
221
|
};
|
|
206
222
|
}
|
|
223
|
+
/** `local` resolves to a Rex pane on this machine; a name with a `rex` entry in user settings to a Rex pane on that SSH host; any other value to a Herdr saved machine. */
|
|
224
|
+
export function resolveMachinePlacement(input) {
|
|
225
|
+
const requested = validateMachineName(input.machine);
|
|
226
|
+
const settings = input.settings ?? readMachineSettings(input.cwd, [requested]);
|
|
227
|
+
if (requested === REX_LOCAL_MACHINE) {
|
|
228
|
+
if (settings?.rex)
|
|
229
|
+
throw new Error("Machine 'local' is reserved for a Rex pane on this machine; give the Rex SSH host in subagents.machines.local.rex another name.");
|
|
230
|
+
return { machine: resolveRexLocalPlacement({ cwd: input.cwd, stepCwd: input.stepCwd, env: input.env }) };
|
|
231
|
+
}
|
|
232
|
+
if (settings?.rex)
|
|
233
|
+
return { machine: { provider: "rex", id: requested, target: settings.rex.ssh, cwd: validateRemoteCwd(remoteMachineCwd(input.stepCwd, settings.cwd, requested), requested) } };
|
|
234
|
+
return resolveHerdrMachinePlacement(input);
|
|
235
|
+
}
|
|
207
236
|
export function formatHerdrMachineRunnerUnsupported(input) {
|
|
208
237
|
if (input.machine === undefined)
|
|
209
238
|
return undefined;
|
|
239
|
+
const name = input.machine.trim();
|
|
240
|
+
let rex;
|
|
241
|
+
try {
|
|
242
|
+
rex = name === REX_LOCAL_MACHINE || userRexMachine(name);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return error instanceof Error ? error.message : String(error);
|
|
246
|
+
}
|
|
247
|
+
if (rex) {
|
|
248
|
+
if (input.runnerType !== undefined && input.runnerType !== "pi")
|
|
249
|
+
return `Agent '${input.agentName}' requested machine '${name}', but Rex supports native Pi children only. Remove the machine or use a native Pi agent.`;
|
|
250
|
+
if (input.worktree === true)
|
|
251
|
+
return `Agent '${input.agentName}' requested machine '${name}', but managed worktrees cannot be combined with machine placement.`;
|
|
252
|
+
if (process.platform === "win32")
|
|
253
|
+
return `Machine '${name}' runs the child in a Rex pane, which is not supported on Windows.`;
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
210
256
|
if (input.runnerType !== undefined && input.runnerType !== "pi" && input.runnerType !== "external-cli") {
|
|
211
257
|
return `Agent '${input.agentName}' requested machine '${input.machine}', but this runner cannot use pane-native Herdr placement. Use native Pi or a built-in Claude, Codex, or Cursor profile.`;
|
|
212
258
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { HerdrMachineReference, PlacementMachineReference } from "../../shared/types.ts";
|
|
2
|
+
import { type HerdrForwardedConnection, type HerdrRpcClient } from "./herdr-connection.ts";
|
|
3
|
+
import type { HerdrRunIdentity } from "./herdr-placed-run.ts";
|
|
4
|
+
import { PlacedRunOwner, type PlacedBridgeManifest } from "./placed-run-owner.ts";
|
|
5
|
+
import { type PlacementAgentKind, type PlacementBridgeIdentity, type PlacementEvidenceProof, type PlacementOwnedPane, type PlacementReconnectCheck, type PlacementRunState, type PlacementStartOptions } from "./placement-host.ts";
|
|
6
|
+
export interface HerdrPaneCandidate {
|
|
7
|
+
endpoint: {
|
|
8
|
+
session: string | null;
|
|
9
|
+
protocol: number;
|
|
10
|
+
version: string;
|
|
11
|
+
};
|
|
12
|
+
agents: Array<{
|
|
13
|
+
terminal_id?: string;
|
|
14
|
+
pane_id?: string;
|
|
15
|
+
agent_status?: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface HerdrReconnectCandidate extends HerdrPaneCandidate {
|
|
19
|
+
bridge: PlacementBridgeIdentity;
|
|
20
|
+
}
|
|
21
|
+
export declare function herdrStatusSubscriptions(paneId: string): unknown[];
|
|
22
|
+
export declare function reconcileHerdrPlacedRun(identity: HerdrRunIdentity, candidate: HerdrPaneCandidate, proveEvidence: () => PlacementEvidenceProof): PlacementReconnectCheck;
|
|
23
|
+
export declare function parseHerdrCreated(value: unknown, type: "workspace_created" | "tab_created"): {
|
|
24
|
+
workspaceId?: string;
|
|
25
|
+
tabId: string;
|
|
26
|
+
paneId: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function parseHerdrAgent(value: unknown, type: "agent_started" | "agent_info" | "agent_prompted"): Record<string, unknown>;
|
|
29
|
+
export declare function parseHerdrSessionSnapshot(value: unknown): Record<string, unknown>;
|
|
30
|
+
export declare function ownsHerdrPane(agent: Record<string, unknown>, terminalId: string, paneId: string): boolean;
|
|
31
|
+
export declare const HERDR_MAX_OWNED_PANES = 20;
|
|
32
|
+
export declare function provisionHerdrPane(client: HerdrRpcClient, cwd: string, runId: string, runtimeDir: string, environment?: Record<string, string>, allocationKey?: string): Promise<{
|
|
33
|
+
workspaceId: string;
|
|
34
|
+
tabId: string;
|
|
35
|
+
paneId: string;
|
|
36
|
+
}>;
|
|
37
|
+
declare function closeOwnedHerdrPane(connection: HerdrForwardedConnection, owned: PlacementOwnedPane, terminalId: string | undefined, agentName: string | undefined): Promise<void>;
|
|
38
|
+
/** Herdr pane host: Herdr RPC over the SSH-forwarded Herdr socket. Ownership is the Herdr terminal_id/pane_id pair; run state comes from agent_status. */
|
|
39
|
+
export declare const herdrPaneHost: {
|
|
40
|
+
connect(machine: PlacementMachineReference): Promise<HerdrForwardedConnection>;
|
|
41
|
+
sessionOf(connection: HerdrForwardedConnection): string | null;
|
|
42
|
+
launch(connection: HerdrForwardedConnection, machine: PlacementMachineReference, runId: string, runtimeDir: string, request: import("./placement-host.ts").PlacementLaunchRequest, onPane: (owned: PlacementOwnedPane) => Promise<void>, options: PlacementStartOptions | undefined): Promise<{
|
|
43
|
+
owned: PlacementOwnedPane;
|
|
44
|
+
terminalId: string;
|
|
45
|
+
}>;
|
|
46
|
+
prompt(connection: HerdrForwardedConnection, identity: HerdrRunIdentity, text: string): Promise<void>;
|
|
47
|
+
subscribe(connection: HerdrForwardedConnection, paneId: string, listener: (event: unknown) => void, onDisconnect: (error: Error) => void): Promise<() => void>;
|
|
48
|
+
paneEvent(event: unknown, fallback: PlacementRunState): {
|
|
49
|
+
terminalId: unknown;
|
|
50
|
+
paneId: unknown;
|
|
51
|
+
state: PlacementRunState;
|
|
52
|
+
} | undefined;
|
|
53
|
+
panes(connection: HerdrForwardedConnection): Promise<{
|
|
54
|
+
endpoint: import("./herdr-connection.ts").HerdrEndpoint;
|
|
55
|
+
agents: {
|
|
56
|
+
terminal_id?: string;
|
|
57
|
+
pane_id?: string;
|
|
58
|
+
agent_status?: string;
|
|
59
|
+
}[];
|
|
60
|
+
}>;
|
|
61
|
+
reconcile: typeof reconcileHerdrPlacedRun;
|
|
62
|
+
closeOwnedPane: typeof closeOwnedHerdrPane;
|
|
63
|
+
};
|
|
64
|
+
/** SSH entry points for the host-neutral runtime-dir and manifest helpers. */
|
|
65
|
+
export declare function discoverBridgeManifest(machine: HerdrMachineReference, runId: string, runtimeDir: string, options?: {
|
|
66
|
+
sshBin?: string;
|
|
67
|
+
env?: NodeJS.ProcessEnv;
|
|
68
|
+
}): Promise<PlacedBridgeManifest>;
|
|
69
|
+
export declare function createRemoteRuntimeDir(machine: HerdrMachineReference, runId: string, options?: {
|
|
70
|
+
sshBin?: string;
|
|
71
|
+
env?: NodeJS.ProcessEnv;
|
|
72
|
+
}): Promise<string>;
|
|
73
|
+
export declare function removeRemoteRuntimeDir(machine: HerdrMachineReference, runtimeDir: string, options?: {
|
|
74
|
+
sshBin?: string;
|
|
75
|
+
env?: NodeJS.ProcessEnv;
|
|
76
|
+
}): void;
|
|
77
|
+
/** The placed-run owner bound to Herdr and SSH access, plus the separate provision and start steps the external CLI adapters use. */
|
|
78
|
+
export declare class HerdrPlacedRunOwner extends PlacedRunOwner<HerdrForwardedConnection, HerdrMachineReference> {
|
|
79
|
+
private constructor();
|
|
80
|
+
static create(machine: HerdrMachineReference, runId: string, beforePane?: (owner: HerdrPlacedRunOwner) => void | Promise<void>): Promise<HerdrPlacedRunOwner>;
|
|
81
|
+
provision(environment?: Record<string, string>): Promise<PlacementOwnedPane>;
|
|
82
|
+
start(kind: PlacementAgentKind, name: string, args: string[], options?: PlacementStartOptions): Promise<{
|
|
83
|
+
terminalId: string;
|
|
84
|
+
paneId: string;
|
|
85
|
+
argv: string[];
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
export {};
|