pi-subagents 0.67.0 → 0.68.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +1 -1
  3. package/docs/agents.md +37 -12
  4. package/docs/configuration.md +61 -19
  5. package/docs/extension-api.md +5 -1
  6. package/docs/missions.md +2 -2
  7. package/docs/models.md +11 -79
  8. package/docs/observability.md +18 -8
  9. package/docs/standalone-background.md +13 -3
  10. package/docs/tool-reference.md +15 -12
  11. package/docs/watchdog.md +10 -12
  12. package/docs/workflows.md +11 -1
  13. package/index.ts +5 -2
  14. package/package.json +4 -2
  15. package/runner-peer-loader.mjs +24 -0
  16. package/runner-peer-preload.mjs +25 -11
  17. package/skills/pi-subagents/SKILL.md +18 -21
  18. package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
  19. package/skills/pi-subagents/references/execution-controls.md +4 -4
  20. package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
  21. package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  22. package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
  23. package/skills/pi-subagents/references/review-and-validation.md +3 -3
  24. package/src/agents/agent-management.ts +57 -58
  25. package/src/agents/agent-serializer.ts +4 -3
  26. package/src/agents/agents.ts +184 -71
  27. package/src/agents/chain-serializer.ts +5 -0
  28. package/src/agents/runtime-agent-registry.ts +7 -6
  29. package/src/api/preflight.ts +20 -16
  30. package/src/api/required-child-extensions.ts +6 -0
  31. package/src/extension/config.ts +10 -37
  32. package/src/extension/fanout-child.ts +3 -0
  33. package/src/extension/herdr-pi-bridge.ts +160 -0
  34. package/src/extension/index.ts +42 -31
  35. package/src/extension/public-execution.ts +3 -3
  36. package/src/extension/schemas.ts +16 -5
  37. package/src/extension/tool-description.ts +8 -7
  38. package/src/intercom/native-supervisor-channel.ts +22 -18
  39. package/src/policy/authority.ts +4 -0
  40. package/src/profiles/profiles.ts +12 -6
  41. package/src/runs/background/active-run-index.ts +17 -1
  42. package/src/runs/background/async-execution.ts +309 -126
  43. package/src/runs/background/async-job-tracker.ts +8 -6
  44. package/src/runs/background/async-resume.ts +13 -4
  45. package/src/runs/background/async-status.ts +15 -4
  46. package/src/runs/background/auto-drain.ts +20 -10
  47. package/src/runs/background/binary-bootstrap.ts +5 -0
  48. package/src/runs/background/chain-append.ts +1 -1
  49. package/src/runs/background/chain-root-attachment.ts +14 -33
  50. package/src/runs/background/notify.ts +74 -6
  51. package/src/runs/background/result-files.ts +8 -4
  52. package/src/runs/background/result-watcher.ts +19 -2
  53. package/src/runs/background/run-child-session.ts +20 -29
  54. package/src/runs/background/runner-aliases.ts +4 -33
  55. package/src/runs/background/runner-child-launch.ts +4 -1
  56. package/src/runs/background/runner-child-sessions.ts +2 -2
  57. package/src/runs/background/runner-http-dispatcher.ts +119 -0
  58. package/src/runs/background/scheduled-runs.ts +11 -5
  59. package/src/runs/background/stale-run-reconciler.ts +35 -11
  60. package/src/runs/background/subagent-runner.ts +396 -275
  61. package/src/runs/background/subagent-wait.ts +128 -23
  62. package/src/runs/background/wait-completions.ts +75 -27
  63. package/src/runs/background/wait-subscriptions.ts +9 -3
  64. package/src/runs/background/wait-tool.ts +4 -2
  65. package/src/runs/foreground/async-stop-action.ts +93 -3
  66. package/src/runs/foreground/execution.ts +91 -218
  67. package/src/runs/foreground/foreground-history.ts +2 -1
  68. package/src/runs/foreground/subagent-executor.ts +266 -80
  69. package/src/runs/shared/acceptance.ts +34 -10
  70. package/src/runs/shared/async-status-projection.ts +123 -33
  71. package/src/runs/shared/child-launch-plan.ts +15 -3
  72. package/src/runs/shared/child-launch.ts +19 -6
  73. package/src/runs/shared/child-runtime-config.ts +5 -0
  74. package/src/runs/shared/child-session.ts +94 -50
  75. package/src/runs/shared/child-tool-plan.ts +28 -16
  76. package/src/runs/shared/dynamic-fanout.ts +2 -2
  77. package/src/runs/shared/external-cli-contract.ts +11 -1
  78. package/src/runs/shared/external-cli-preflight.ts +6 -2
  79. package/src/runs/shared/herdr-connection.ts +134 -0
  80. package/src/runs/shared/herdr-external-adapters.ts +169 -0
  81. package/src/runs/shared/herdr-machine.ts +279 -0
  82. package/src/runs/shared/herdr-pi-protocol.ts +59 -0
  83. package/src/runs/shared/herdr-placed-run.ts +263 -0
  84. package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
  85. package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
  86. package/src/runs/shared/model-scope.ts +1 -1
  87. package/src/runs/shared/nested-events.ts +11 -2
  88. package/src/runs/shared/parallel-utils.ts +7 -2
  89. package/src/runs/shared/pi-spawn.ts +1 -1
  90. package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  91. package/src/runs/shared/worktree-setup-command.ts +27 -4
  92. package/src/runs/shared/worktree.ts +3 -3
  93. package/src/shared/child-cache-retention.ts +43 -0
  94. package/src/shared/launch-contract.ts +6 -9
  95. package/src/shared/pruned-fork.ts +1 -1
  96. package/src/shared/required-child-extensions.ts +81 -0
  97. package/src/shared/settings.ts +5 -2
  98. package/src/shared/shortcuts.ts +0 -4
  99. package/src/shared/types.ts +70 -29
  100. package/src/slash/slash-commands.ts +0 -6
  101. package/src/slash/subagents-admin.ts +13 -9
  102. package/src/tui/render.ts +20 -10
  103. package/src/watchdog/child-status.ts +28 -36
  104. package/src/watchdog/lsp-diagnostics.ts +1 -1
  105. package/src/watchdog/model-selection.ts +1 -1
  106. package/src/watchdog/register-child.ts +10 -3
  107. package/src/watchdog/register-main.ts +20 -20
  108. package/src/watchdog/render.ts +1 -1
  109. package/src/watchdog/review.ts +14 -30
  110. package/src/watchdog/rules.ts +1 -1
  111. package/src/watchdog/runtime.ts +23 -12
  112. package/src/watchdog/settings.ts +3 -6
  113. package/src/watchdog/types.ts +3 -5
  114. package/src/watchdog/warning-format.ts +1 -1
  115. package/src/workflows/scripted-workflow.ts +42 -3
  116. package/src/workflows/workflow-receipt.ts +21 -3
  117. package/src/workflows/workflow-resources.ts +13 -2
  118. package/src/runs/shared/model-exclusions.ts +0 -374
  119. package/src/runs/shared/readonly-model-continuation.ts +0 -69
  120. package/src/runs/shared/readonly-session-evidence.ts +0 -307
@@ -0,0 +1,279 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import type { ExternalProcessStatus, HerdrMachineReference } from "../../shared/types.ts";
5
+ import { getAgentDir, getProjectConfigDir } from "../../shared/utils.ts";
6
+ import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS, type CodeOwnedExternalCliAdapterId } from "./external-cli-contract.ts";
7
+ import type { runExternalCli } from "./external-cli-runner.ts";
8
+ export { shellQuoteRemote as shellQuote } from "./herdr-connection.ts";
9
+
10
+ /**
11
+ * Herdr saved-machine placement for external CLI children.
12
+ *
13
+ * Herdr owns which machines exist and how ssh reaches them (`herdr machine list --json`).
14
+ * pi-subagents owns what runs there: native Pi and the six code-owned external profiles are
15
+ * launched in fresh Herdr-owned visible panes. SSH is bounded transport and never owns the agent.
16
+ * `cwd` means the directory on that machine; the remote `cd` is the directory check.
17
+ */
18
+
19
+ const MAX_MACHINE_NAME_LENGTH = 128;
20
+ const HERDR_MACHINE_LIST_TIMEOUT_MS = 7_500;
21
+ const MAX_HERDR_MACHINE_LIST_BYTES = 1024 * 1024;
22
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/u;
23
+ const SUPPORTED_MACHINE_ADAPTERS = new Set<string>(CODE_OWNED_EXTERNAL_CLI_ADAPTER_IDS);
24
+ /** The local ssh process gets only what ssh itself needs; remote runs use the machine's own credentials. */
25
+ export const HERDR_SSH_ENV_ALLOWLIST = ["PATH", "HOME", "USER", "LOGNAME", "TMPDIR", "SSH_AUTH_SOCK"] as const;
26
+
27
+ type RunExternalCliInput = Parameters<typeof runExternalCli>[0];
28
+
29
+ interface HerdrMachineCatalogEntry {
30
+ id: string;
31
+ label?: string;
32
+ target: string;
33
+ session?: string;
34
+ enabled: boolean;
35
+ }
36
+
37
+ interface MachineSettingsEntry {
38
+ cwd?: string;
39
+ env?: Record<string, string>;
40
+ }
41
+
42
+ export interface ResolveHerdrMachinePlacementInput {
43
+ /** Profile id or label as typed by the operator. */
44
+ machine: string;
45
+ /** Local directory whose project settings hold `subagents.machines`. */
46
+ cwd: string;
47
+ /** Launch cwd: absolute or `~` paths are remote paths as given; relative paths join the configured machine root. */
48
+ stepCwd?: string;
49
+ env?: NodeJS.ProcessEnv;
50
+ /** Test seam: catalog JSON instead of spawning `herdr machine list --json`. */
51
+ catalogJson?: string;
52
+ /** Test seam: settings entry instead of reading settings files. */
53
+ settings?: MachineSettingsEntry;
54
+ herdrBin?: string;
55
+ }
56
+
57
+ export interface HerdrMachinePlacement {
58
+ machine: HerdrMachineReference;
59
+ /** Opt-in `subagents.machines.<name>.env`, exported in front of the remote command. */
60
+ env?: Record<string, string>;
61
+ }
62
+
63
+ export interface PreparedHerdrMachineExternalCliRun {
64
+ input: RunExternalCliInput;
65
+ decorateProcess(process: ExternalProcessStatus): ExternalProcessStatus;
66
+ }
67
+
68
+ function validateMachineName(value: string): string {
69
+ const trimmed = value.trim();
70
+ if (!trimmed) throw new Error("Herdr machine id or label is required.");
71
+ if (trimmed.length > MAX_MACHINE_NAME_LENGTH) throw new Error(`Herdr machine '${trimmed.slice(0, 24)}…' exceeds ${MAX_MACHINE_NAME_LENGTH} characters.`);
72
+ if (CONTROL_CHARS.test(trimmed)) throw new Error("Herdr machine id or label contains control characters.");
73
+ return trimmed;
74
+ }
75
+
76
+ function validateTarget(value: string, requested: string): string {
77
+ const target = value.trim();
78
+ if (!target || target.startsWith("-") || /[\s\u0000-\u001f\u007f]/u.test(target)) {
79
+ throw new Error(`Herdr machine '${requested}' has an ssh target that cannot be passed safely: ${JSON.stringify(target)}.`);
80
+ }
81
+ return target;
82
+ }
83
+
84
+ function isRemoteAbsolute(value: string): boolean {
85
+ return value.startsWith("/") || value === "~" || value.startsWith("~/");
86
+ }
87
+
88
+ function validateRemoteCwd(value: string, requested: string): string {
89
+ const cwd = value.trim();
90
+ if (!isRemoteAbsolute(cwd)) throw new Error(`Herdr machine '${requested}' cwd must be an absolute POSIX path or start with '~': ${JSON.stringify(cwd)}.`);
91
+ if (CONTROL_CHARS.test(cwd)) throw new Error(`Herdr machine '${requested}' cwd contains control characters.`);
92
+ return cwd.length > 1 ? cwd.replace(/\/+$/u, "") : cwd;
93
+ }
94
+
95
+ function parseMachineCatalog(json: string): HerdrMachineCatalogEntry[] {
96
+ let parsed: unknown;
97
+ try {
98
+ parsed = JSON.parse(json) as unknown;
99
+ } catch (error) {
100
+ throw new Error(`Failed to parse herdr machine list --json: ${error instanceof Error ? error.message : String(error)}`);
101
+ }
102
+ const entries = Array.isArray(parsed) ? parsed : (parsed && typeof parsed === "object" && Array.isArray((parsed as Record<string, unknown>).machines) ? (parsed as Record<string, unknown[]>).machines : undefined);
103
+ if (!entries) throw new Error("herdr machine list --json returned no machine list.");
104
+ return entries.flatMap((entry) => {
105
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return [];
106
+ const record = entry as Record<string, unknown>;
107
+ if (typeof record.id !== "string" || !record.id.trim() || typeof record.target !== "string") return [];
108
+ return [{
109
+ id: record.id.trim(),
110
+ ...(typeof record.label === "string" && record.label.trim() ? { label: record.label.trim() } : {}),
111
+ target: record.target,
112
+ ...(typeof record.session === "string" && record.session.trim() ? { session: record.session.trim() } : {}),
113
+ enabled: record.enabled !== false,
114
+ }];
115
+ });
116
+ }
117
+
118
+ function readHerdrMachineCatalog(env: NodeJS.ProcessEnv, herdrBin: string): HerdrMachineCatalogEntry[] {
119
+ const result = spawnSync(herdrBin, ["machine", "list", "--json"], {
120
+ env,
121
+ encoding: "utf-8",
122
+ maxBuffer: MAX_HERDR_MACHINE_LIST_BYTES,
123
+ timeout: HERDR_MACHINE_LIST_TIMEOUT_MS,
124
+ windowsHide: true,
125
+ });
126
+ if (result.error) {
127
+ const code = (result.error as NodeJS.ErrnoException).code;
128
+ if (code === "ENOENT") throw new Error(`Herdr CLI '${herdrBin}' was not found on PATH. Saved-machine placement needs Herdr installed locally.`);
129
+ throw new Error(`Failed to run herdr machine list --json: ${result.error.message}`);
130
+ }
131
+ if (result.status !== 0) throw new Error(`herdr machine list --json exited with code ${result.status}: ${(result.stderr || result.stdout).trim()}`);
132
+ return parseMachineCatalog(result.stdout);
133
+ }
134
+
135
+ /** Mirrors Herdr's own selector rules: profile id first, then a unique case-sensitive label; disabled fails closed. */
136
+ function selectMachine(catalog: HerdrMachineCatalogEntry[], requested: string): HerdrMachineCatalogEntry {
137
+ const byId = catalog.find((entry) => entry.id === requested);
138
+ const matches = byId ? [byId] : catalog.filter((entry) => entry.label === requested);
139
+ if (matches.length > 1) throw new Error(`Machine label '${requested}' is ambiguous; use its profile ID.`);
140
+ const machine = matches[0];
141
+ if (!machine) {
142
+ const saved = catalog.filter((entry) => entry.enabled).map((entry) => entry.label ?? entry.id);
143
+ throw new Error(`Herdr machine '${requested}' was not found. Saved machines: ${saved.length ? saved.join(", ") : "none"}. Add one with herdr machine add <target> --label <name>.`);
144
+ }
145
+ if (!machine.enabled) throw new Error(`Machine '${requested}' is disabled. Run herdr machine enable ${machine.id}.`);
146
+ return machine;
147
+ }
148
+
149
+ function readJsonObject(filePath: string): Record<string, unknown> {
150
+ if (!fs.existsSync(filePath)) return {};
151
+ let parsed: unknown;
152
+ try {
153
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
154
+ } catch (error) {
155
+ throw new Error(`Failed to read settings file '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
156
+ }
157
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
158
+ }
159
+
160
+ function machineSettingsFrom(settings: Record<string, unknown>, keys: readonly string[], filePath: string): MachineSettingsEntry | undefined {
161
+ const subagents = settings.subagents;
162
+ if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return undefined;
163
+ const machines = (subagents as Record<string, unknown>).machines;
164
+ if (machines === undefined) return undefined;
165
+ if (!machines || typeof machines !== "object" || Array.isArray(machines)) throw new Error(`Subagent settings in '${filePath}' have invalid 'machines'; expected an object keyed by machine label or id.`);
166
+ const key = keys.find((candidate) => candidate in (machines as Record<string, unknown>));
167
+ if (key === undefined) return undefined;
168
+ const value = (machines as Record<string, unknown>)[key];
169
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Subagent settings in '${filePath}' have invalid 'machines.${key}'; expected an object with 'cwd' and optional 'env'.`);
170
+ const record = value as Record<string, unknown>;
171
+ const entry: MachineSettingsEntry = {};
172
+ if (record.cwd !== undefined) {
173
+ if (typeof record.cwd !== "string" || !record.cwd.trim()) throw new Error(`Subagent settings in '${filePath}' have invalid 'machines.${key}.cwd'; expected a non-empty string.`);
174
+ entry.cwd = validateRemoteCwd(record.cwd, key);
175
+ }
176
+ if (record.env !== undefined) {
177
+ if (!record.env || typeof record.env !== "object" || Array.isArray(record.env) || Object.entries(record.env).some(([name, item]) => !/^[A-Za-z_][A-Za-z0-9_]*$/u.test(name) || typeof item !== "string")) {
178
+ throw new Error(`Subagent settings in '${filePath}' have invalid 'machines.${key}.env'; expected an object of string values keyed by variable name.`);
179
+ }
180
+ entry.env = { ...(record.env as Record<string, string>) };
181
+ if (Object.keys(entry.env).length > 0) {
182
+ 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.`);
183
+ }
184
+ }
185
+ return entry;
186
+ }
187
+
188
+ /** Project settings beat user settings, field by field; the entry may be keyed by label, id, or the typed selector. */
189
+ function readMachineSettings(cwd: string, keys: readonly string[]): MachineSettingsEntry | undefined {
190
+ const userPath = path.join(getAgentDir(), "settings.json");
191
+ const projectPath = path.join(getProjectConfigDir(findProjectRootForSettings(cwd)), "settings.json");
192
+ const user = machineSettingsFrom(readJsonObject(userPath), keys, userPath);
193
+ const project = projectPath === userPath ? undefined : machineSettingsFrom(readJsonObject(projectPath), keys, projectPath);
194
+ if (!user && !project) return undefined;
195
+ return { ...user, ...project };
196
+ }
197
+
198
+ function findProjectRootForSettings(cwd: string): string {
199
+ let current = path.resolve(cwd);
200
+ while (true) {
201
+ if (fs.existsSync(path.join(getProjectConfigDir(current), "settings.json"))) return current;
202
+ const parent = path.dirname(current);
203
+ if (parent === current) return path.resolve(cwd);
204
+ current = parent;
205
+ }
206
+ }
207
+
208
+ export function resolveHerdrMachinePlacement(input: ResolveHerdrMachinePlacementInput): HerdrMachinePlacement {
209
+ const requested = validateMachineName(input.machine);
210
+ const catalog = input.catalogJson !== undefined
211
+ ? parseMachineCatalog(input.catalogJson)
212
+ : readHerdrMachineCatalog(input.env ?? process.env, input.herdrBin ?? process.env.HERDR_BIN ?? "herdr");
213
+ const selected = selectMachine(catalog, requested);
214
+ const name = selected.label ?? selected.id;
215
+ const keys = [...new Set([requested, ...(selected.label ? [selected.label] : []), selected.id])];
216
+ const settings = input.settings ?? readMachineSettings(input.cwd, keys);
217
+ if (settings?.env && Object.keys(settings.env).length > 0) {
218
+ throw new Error(`Saved-machine environment for '${name}' must be configured remotely. Remove machines.${name}.env and configure the remote Herdr/Pi session instead.`);
219
+ }
220
+ const stepCwd = input.stepCwd?.trim();
221
+ let cwd: string;
222
+ if (stepCwd && isRemoteAbsolute(stepCwd)) cwd = stepCwd;
223
+ else if (settings?.cwd) cwd = stepCwd ? path.posix.join(settings.cwd, stepCwd) : settings.cwd;
224
+ else 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.`);
225
+ return {
226
+ machine: {
227
+ provider: "herdr",
228
+ id: selected.id,
229
+ ...(selected.label ? { label: selected.label } : {}),
230
+ target: validateTarget(selected.target, requested),
231
+ ...(selected.session && selected.session !== "default" ? { session: selected.session } : {}),
232
+ cwd: validateRemoteCwd(cwd, requested),
233
+ },
234
+ ...(settings?.env ? { env: settings.env } : {}),
235
+ };
236
+ }
237
+
238
+ export function formatHerdrMachineRunnerUnsupported(input: {
239
+ machine?: string;
240
+ agentName: string;
241
+ runnerType?: string;
242
+ adapter?: string;
243
+ worktree?: boolean;
244
+ }): string | undefined {
245
+ if (input.machine === undefined) return undefined;
246
+ if (input.runnerType !== undefined && input.runnerType !== "pi" && input.runnerType !== "external-cli") {
247
+ 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.`;
248
+ }
249
+ if (input.runnerType === "external-cli" && (input.adapter === undefined || !SUPPORTED_MACHINE_ADAPTERS.has(input.adapter))) {
250
+ return `Agent '${input.agentName}' requested machine '${input.machine}', but generic external-cli commands cannot be remote-wrapped safely. Use claude-code, claude-code-writer, codex-exec, codex-exec-writer, cursor-agent, or cursor-agent-writer.`;
251
+ }
252
+ if (input.worktree === true) return `Agent '${input.agentName}' requested machine '${input.machine}', but managed worktrees are local git operations and cannot be combined with a Herdr saved machine.`;
253
+ if (process.platform === "win32") return "Herdr saved-machine pane transport requires hardened OpenSSH StreamLocal forwarding, which is not supported from a Windows host yet.";
254
+ return undefined;
255
+ }
256
+
257
+ /** Legacy local-child SSH wrapping is intentionally unavailable after the pane-native cut-over. */
258
+ export function prepareHerdrMachineExternalCliRun(input: RunExternalCliInput, placement: HerdrMachinePlacement | undefined, _options: { localCwd: string }): PreparedHerdrMachineExternalCliRun {
259
+ if (placement) throw new Error("Saved-machine external profiles must run in a Herdr-owned pane.");
260
+ return { input, decorateProcess: (process) => process };
261
+ }
262
+ const HINTS: ReadonlyArray<readonly [RegExp, (machine: HerdrMachineReference) => string]> = [
263
+ [/Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED|Permission denied \(publickey|Permission denied, please try again|No such identity|Could not resolve hostname|Connection (timed out|refused)/iu,
264
+ (machine) => `ssh could not reach or authenticate with ${machine.target}. Connect once interactively with ssh ${machine.target} to accept the host key or fix the identity; BatchMode never prompts.`],
265
+ [/is not recognized as|CommandNotFoundException|PowerShell|At line:\d+ char:\d+/u,
266
+ (machine) => `Machine '${machine.label ?? machine.id}' is not a POSIX host. External-cli runs support POSIX ssh targets only.`],
267
+ [/\bcd: .*(No such file or directory|not a directory|can't cd)|exit(?:ed with)? code 125\b/iu,
268
+ (machine) => `Nothing at ${machine.cwd} on ${machine.label ?? machine.id}. Clone the repo there first; pi-subagents never clones, pulls, or checks out on a machine.`],
269
+ [/(?:command not found|not found)\s*$|No such file or directory\s*$|exit(?:ed with)? code 127\b/imu,
270
+ (machine) => `The agent CLI was not found on ${machine.label ?? machine.id}. Non-interactive shells skip rc files and the PATH prefix did not find it; set the agent's command to the absolute path on that machine.`],
271
+ [/not logged in|unauthorized|authentication_error|invalid api key|please run .*login|OAuth token/iu,
272
+ (machine) => `Remote runs use the machine's own credentials. Log in to the agent CLI on ${machine.label ?? machine.id} once.`],
273
+ ];
274
+
275
+ /** One-line operator hint for a predictable remote failure, matched against error text and the stderr tail. */
276
+ export function formatHerdrMachineHint(machine: HerdrMachineReference, text: string): string | undefined {
277
+ for (const [pattern, hint] of HINTS) if (pattern.test(text)) return hint(machine);
278
+ return undefined;
279
+ }
@@ -0,0 +1,59 @@
1
+ import * as path from "node:path";
2
+
3
+ export const HERDR_PI_PROTOCOL = 1;
4
+ export const HERDR_PI_MAX_FRAME_BYTES = 1024 * 1024;
5
+ export const HERDR_PI_MODE_ENV = "PI_SUBAGENTS_HERDR_BRIDGE";
6
+ export const HERDR_PI_RUN_ENV = "PI_SUBAGENTS_HERDR_RUN_ID";
7
+ export const HERDR_PI_RUNTIME_DIR_ENV = "PI_SUBAGENTS_HERDR_RUNTIME_DIR";
8
+ const SAFE_ID = /^[A-Za-z0-9_-]{8,96}$/u;
9
+
10
+ export function validateHerdrPiRunId(value: unknown): string {
11
+ if (typeof value !== "string" || !SAFE_ID.test(value)) throw new Error("Herdr Pi bridge run identity is invalid.");
12
+ return value;
13
+ }
14
+
15
+ export function herdrPiRuntimeRoot(agentDir: string): string {
16
+ return path.join(agentDir, "pi-subagents", "herdr-runs");
17
+ }
18
+
19
+ export function herdrPiRunDir(agentDir: string, runId: string): string {
20
+ return path.join(herdrPiRuntimeRoot(agentDir), validateHerdrPiRunId(runId));
21
+ }
22
+
23
+ export interface HerdrPiFrame {
24
+ protocol: number;
25
+ runId: string;
26
+ type: string;
27
+ requestId?: string;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ export function encodeHerdrPiFrame(frame: HerdrPiFrame): Buffer {
32
+ const data = Buffer.from(`${JSON.stringify(frame)}\n`, "utf8");
33
+ if (data.byteLength > HERDR_PI_MAX_FRAME_BYTES) throw new Error(`Herdr Pi frame exceeds ${HERDR_PI_MAX_FRAME_BYTES} bytes.`);
34
+ return data;
35
+ }
36
+
37
+ export class HerdrPiFrameDecoder {
38
+ #buffer = Buffer.alloc(0);
39
+ push(chunk: Buffer): HerdrPiFrame[] {
40
+ this.#buffer = Buffer.concat([this.#buffer, chunk]);
41
+ if (this.#buffer.byteLength > HERDR_PI_MAX_FRAME_BYTES) throw new Error(`Herdr Pi frame exceeds ${HERDR_PI_MAX_FRAME_BYTES} bytes.`);
42
+ const frames: HerdrPiFrame[] = [];
43
+ for (;;) {
44
+ const newline = this.#buffer.indexOf(10);
45
+ if (newline < 0) break;
46
+ const line = this.#buffer.subarray(0, newline);
47
+ this.#buffer = this.#buffer.subarray(newline + 1);
48
+ let value: unknown;
49
+ try { value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(line)) as unknown; } catch { throw new Error("Herdr Pi bridge emitted malformed JSON or UTF-8."); }
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Herdr Pi bridge frame must be an object.");
51
+ const frame = value as HerdrPiFrame;
52
+ if (frame.protocol !== HERDR_PI_PROTOCOL || typeof frame.type !== "string") throw new Error("Herdr Pi bridge protocol mismatch.");
53
+ validateHerdrPiRunId(frame.runId);
54
+ frames.push(frame);
55
+ }
56
+ return frames;
57
+ }
58
+ end(): void { if (this.#buffer.byteLength) throw new Error("Herdr Pi bridge ended with a partial frame."); }
59
+ }