@voybio/ace-swarm 2.4.1 → 2.4.2

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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { ACE_TASKS_ROOT_REL, ALL_MCP_CLIENTS, ALL_LLM_PROVIDERS, DEFAULTS_ROOT, PACKAGE_ROOT, WORKSPACE_ROOT, fileExists, getAllMcpServerConfigSnippets, getMcpClientInstallHint, getMcpServerConfigSnippet, wsPath, } from "./helpers.js";
3
3
  import { refreshAstgrepIndex } from "./astgrep-index.js";
4
4
  import { scanWorkspaceDelta } from "./index-store.js";
5
- import { startStdioServer } from "./server.js";
5
+ import { startHermesShadowStdioServer, startStdioServer } from "./server.js";
6
6
  import { appendRunLedgerEntrySafe } from "./run-ledger.js";
7
7
  import { appendStatusEventSafe, waitForPendingStatusEventMirrors } from "./status-events.js";
8
8
  import { bootstrapStoreWorkspace } from "./store/bootstrap-store.js";
@@ -12,16 +12,20 @@ import { DiscoveryRepository } from "./store/repositories/discovery-repository.j
12
12
  import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
13
13
  import { getWorkspaceStorePath, readStoreBlobSync, readStoreJsonSync, } from "./store/store-snapshot.js";
14
14
  import { ensureCanonicalWorkspaceStore } from "./store/workspace-store-paths.js";
15
- import { readFileSync } from "node:fs";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { resolve } from "node:path";
17
+ import { spawn } from "node:child_process";
16
18
  import { runTui } from "./tui/index.js";
17
19
  import { buildOpenAiCompatibleBaseUrl, buildProviderDoctorCommands, defaultModelForProvider, discoverProviderContext, isLocalLlmProvider, normalizeLocalBaseUrl, normalizeLlamaCppHfModelName, normalizeProvider, scanLocalModelRuntimes, } from "./tui/provider-discovery.js";
18
20
  import { diagnoseChatRuntimeConfig, OpenAICompatibleClient, } from "./tui/openai-compatible.js";
19
21
  import { runShellCommand } from "./runtime-command.js";
22
+ import { resolveHermesLaunchProfile } from "./hermes/launch-profile.js";
20
23
  function printHelp() {
21
24
  console.log(`ACE Swarm CLI
22
25
 
23
26
  Usage:
24
27
  ace mcp Start MCP server over stdio
28
+ ace mcp-shadow --tools <csv> Start filtered Hermes-local MCP shadow server over stdio
25
29
  ace serve Alias for mcp
26
30
  ace tui [options] Launch interactive TUI dashboard
27
31
  ace init [options] Bootstrap the ACE store into current workspace
@@ -39,6 +43,16 @@ Options for tui:
39
43
  --model <name> Model name override (defaults from profile/settings discovery)
40
44
  --base-url <url> Local runtime base URL override
41
45
  --ollama-url <url> Legacy alias for --base-url
46
+ --engine <name> direct|hermes_local execution engine
47
+ --hermes Alias for --engine hermes_local
48
+ --hermes-root <path> User-supplied Hermes checkout/import root
49
+ --hermes-python <path> User-supplied Python executable with Hermes deps
50
+ --hermes-uv-project <path> Optional uv project root for operator-managed Hermes
51
+ --hermes-command-json <json> Advanced command array override for Hermes Python
52
+
53
+ Hermes is optional and user-supplied. \`ace tui\` and \`ace doctor\` only resolve
54
+ Hermes when you pass \`--hermes\` / \`--engine hermes_local\` or set the matching
55
+ Hermes environment variables.
42
56
 
43
57
  Options for init:
44
58
  --project <name> Project name stored in agent-state/ace-state.ace metadata
@@ -63,6 +77,11 @@ Options for doctor:
63
77
  --base-url <url> Runtime base URL override
64
78
  --ollama-url <url> Legacy alias for --base-url
65
79
  --scan Probe common local Ollama + llama.cpp endpoints when URL is unset
80
+ --hermes Also run optional Hermes-local readiness checks
81
+ --hermes-root <path> User-supplied Hermes checkout/import root (or ACE_HERMES_ROOT)
82
+ --hermes-python <path> User-supplied Python executable (or HERMES_PYTHON)
83
+ --hermes-uv-project <path> Optional uv project root for operator-managed Hermes
84
+ --hermes-command-json <json> Advanced command array override for Hermes Python
66
85
  --repair-ollama Opt-in: run ollama pull <model> when doctor finds a missing Ollama model
67
86
 
68
87
  Options for cache:
@@ -73,6 +92,11 @@ Options for mcp-config:
73
92
  --client <name> codex|vscode|copilot|claude|cursor|antigravity
74
93
  --all Print all client snippets for optional global install
75
94
 
95
+ Hermes environment:
96
+ ACE_HERMES_ROOT User-supplied Hermes checkout/import root
97
+ HERMES_PYTHON User-supplied Python with Hermes dependencies
98
+ ACE_HERMES_UV_PROJECT Optional operator-managed uv project root
99
+
76
100
  preconfig writes .mcp-config/ at the workspace root with ready-to-use config files for every
77
101
  supported MCP client plus a root .mcp.json for GitHub Copilot CLI. Run once after ace init.
78
102
  Each file includes the install hint for its client.
@@ -87,6 +111,180 @@ function readFlagValue(args, flag) {
87
111
  return undefined;
88
112
  return args[index + 1];
89
113
  }
114
+ async function runCommandArray(command, args, options) {
115
+ const [bin, ...prefixArgs] = command;
116
+ if (!bin)
117
+ return { ok: false, stdout: "", stderr: "", detail: "empty command array" };
118
+ return await new Promise((resolveCommand) => {
119
+ const child = spawn(bin, [...prefixArgs, ...args], {
120
+ cwd: options.cwd,
121
+ env: options.env,
122
+ stdio: ["ignore", "pipe", "pipe"],
123
+ });
124
+ let stdout = "";
125
+ let stderr = "";
126
+ const timeout = setTimeout(() => {
127
+ child.kill("SIGTERM");
128
+ }, options.timeoutMs ?? 5000);
129
+ child.stdout?.setEncoding("utf8");
130
+ child.stderr?.setEncoding("utf8");
131
+ child.stdout?.on("data", (chunk) => {
132
+ stdout += String(chunk);
133
+ });
134
+ child.stderr?.on("data", (chunk) => {
135
+ stderr += String(chunk);
136
+ });
137
+ child.on("error", (error) => {
138
+ clearTimeout(timeout);
139
+ resolveCommand({ ok: false, stdout, stderr, detail: error.message });
140
+ });
141
+ child.on("close", (code) => {
142
+ clearTimeout(timeout);
143
+ resolveCommand({
144
+ ok: code === 0,
145
+ stdout,
146
+ stderr,
147
+ detail: code === 0 ? "exit 0" : `exit ${code}; ${stderr.slice(0, 300)}`,
148
+ });
149
+ });
150
+ });
151
+ }
152
+ function mcpFrame(payload) {
153
+ const body = JSON.stringify(payload);
154
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
155
+ }
156
+ async function probeHermesShadowMcp(cliPath, toolAllowlist) {
157
+ return await new Promise((resolveProbe, rejectProbe) => {
158
+ const child = spawn(process.execPath, [cliPath, "mcp-shadow", "--tools", toolAllowlist.join(",")], {
159
+ cwd: PACKAGE_ROOT,
160
+ stdio: ["pipe", "pipe", "pipe"],
161
+ });
162
+ let stdout = "";
163
+ let stderr = "";
164
+ const timeout = setTimeout(() => {
165
+ child.kill("SIGTERM");
166
+ rejectProbe(new Error(`shadow MCP probe timed out${stderr ? `: ${stderr.slice(0, 200)}` : ""}`));
167
+ }, 2500);
168
+ child.stdout.on("data", (chunk) => {
169
+ stdout += chunk.toString("utf8");
170
+ if (stdout.includes('"tools"') && stdout.includes(toolAllowlist[0] ?? "")) {
171
+ clearTimeout(timeout);
172
+ child.kill("SIGTERM");
173
+ resolveProbe(`shadow MCP initialize/tools-list probe succeeded; tools=${toolAllowlist.join(",")}`);
174
+ }
175
+ });
176
+ child.stderr.on("data", (chunk) => {
177
+ stderr += chunk.toString("utf8");
178
+ });
179
+ child.on("error", (error) => {
180
+ clearTimeout(timeout);
181
+ rejectProbe(error);
182
+ });
183
+ child.on("close", () => {
184
+ clearTimeout(timeout);
185
+ if (stdout.includes('"tools"') && stdout.includes(toolAllowlist[0] ?? "")) {
186
+ resolveProbe(`shadow MCP initialize/tools-list probe succeeded; tools=${toolAllowlist.join(",")}`);
187
+ }
188
+ else {
189
+ rejectProbe(new Error(`shadow MCP probe did not return tools/list${stderr ? `: ${stderr.slice(0, 200)}` : ""}`));
190
+ }
191
+ });
192
+ child.stdin.write(mcpFrame({
193
+ jsonrpc: "2.0",
194
+ id: 1,
195
+ method: "initialize",
196
+ params: {
197
+ protocolVersion: "2024-11-05",
198
+ capabilities: {},
199
+ clientInfo: { name: "ace-doctor", version: "0.0.0" },
200
+ },
201
+ }));
202
+ child.stdin.write(mcpFrame({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }));
203
+ child.stdin.write(mcpFrame({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }));
204
+ });
205
+ }
206
+ async function appendHermesLocalReadinessChecks(checks, profile) {
207
+ const hermesRoot = profile.hermes_root;
208
+ const workerPath = profile.worker_path;
209
+ const cliPath = resolve(PACKAGE_ROOT, "dist", "cli.js");
210
+ const toolAllowlist = ["get_all_agents_summary"];
211
+ checks.push({
212
+ name: "Hermes launch profile",
213
+ ok: true,
214
+ detail: `source=${profile.source}; mode=${profile.mode}; command=${JSON.stringify(profile.command)}; hermes_root=${profile.hermes_root ?? "not configured"}`,
215
+ });
216
+ checks.push({
217
+ name: "Hermes worker packaged",
218
+ ok: existsSync(workerPath),
219
+ detail: existsSync(workerPath)
220
+ ? workerPath
221
+ : `Missing ${workerPath}; run npm run build.`,
222
+ });
223
+ checks.push({
224
+ name: "Hermes checkout import root",
225
+ ok: Boolean(hermesRoot &&
226
+ existsSync(resolve(hermesRoot, "run_agent.py")) &&
227
+ existsSync(resolve(hermesRoot, "tools", "mcp_tool.py"))),
228
+ detail: hermesRoot && existsSync(resolve(hermesRoot, "run_agent.py"))
229
+ ? hermesRoot
230
+ : "ACE does not install Hermes. Set ACE_HERMES_ROOT or pass --hermes-root to a Hermes checkout.",
231
+ });
232
+ const pythonResolves = await runCommandArray(profile.command, ["-c", "import sys; print(sys.executable)"], {
233
+ cwd: PACKAGE_ROOT,
234
+ timeoutMs: 5000,
235
+ });
236
+ checks.push({
237
+ name: "Hermes Python command",
238
+ ok: pythonResolves.ok,
239
+ detail: pythonResolves.ok ? `${JSON.stringify(profile.command)} -> ${pythonResolves.stdout.trim()}` : pythonResolves.detail,
240
+ });
241
+ const importEnv = {
242
+ ...process.env,
243
+ PYTHONPATH: [hermesRoot, process.env.PYTHONPATH].filter(Boolean).join(":"),
244
+ };
245
+ const importResult = hermesRoot
246
+ ? await runCommandArray(profile.command, ["-c", "from run_agent import AIAgent; print(AIAgent.__name__)"], {
247
+ cwd: hermesRoot,
248
+ env: importEnv,
249
+ timeoutMs: 5000,
250
+ })
251
+ : { ok: false, detail: "Hermes root not configured", stdout: "", stderr: "" };
252
+ checks.push({
253
+ name: "Hermes Python imports",
254
+ ok: importResult.ok,
255
+ detail: importResult.ok ? "run_agent.AIAgent import ok" : importResult.detail,
256
+ });
257
+ checks.push({
258
+ name: "ACE shadow MCP command",
259
+ ok: existsSync(cliPath),
260
+ detail: existsSync(cliPath)
261
+ ? JSON.stringify([process.execPath, cliPath, "mcp-shadow", "--tools", toolAllowlist.join(",")])
262
+ : `Missing ${cliPath}; run npm run build.`,
263
+ });
264
+ if (existsSync(cliPath)) {
265
+ try {
266
+ checks.push({
267
+ name: "ACE shadow MCP handshake",
268
+ ok: true,
269
+ detail: await probeHermesShadowMcp(cliPath, toolAllowlist),
270
+ });
271
+ }
272
+ catch (error) {
273
+ checks.push({
274
+ name: "ACE shadow MCP handshake",
275
+ ok: false,
276
+ detail: error instanceof Error ? error.message : String(error),
277
+ });
278
+ }
279
+ }
280
+ else {
281
+ checks.push({
282
+ name: "ACE shadow MCP handshake",
283
+ ok: false,
284
+ detail: `Cannot probe shadow MCP until ${cliPath} exists; run npm run build.`,
285
+ });
286
+ }
287
+ }
90
288
  function readLlmProfile() {
91
289
  const storeProfile = readStoreJsonSync(WORKSPACE_ROOT, "state/runtime/llm_profile") ??
92
290
  readStoreJsonSync(WORKSPACE_ROOT, "state/runtime/llm-profile");
@@ -654,6 +852,25 @@ async function runDoctor(args) {
654
852
  }
655
853
  }
656
854
  }
855
+ if (isLocalLlmProvider(provider) && args.includes("--hermes")) {
856
+ try {
857
+ const hermesProfile = resolveHermesLaunchProfile({
858
+ workspaceRoot: WORKSPACE_ROOT,
859
+ cliHermesRoot: readFlagValue(args, "--hermes-root"),
860
+ cliHermesPython: readFlagValue(args, "--hermes-python"),
861
+ cliHermesUvProject: readFlagValue(args, "--hermes-uv-project"),
862
+ cliHermesCommandJson: readFlagValue(args, "--hermes-command-json"),
863
+ });
864
+ await appendHermesLocalReadinessChecks(checks, hermesProfile);
865
+ }
866
+ catch (error) {
867
+ checks.push({
868
+ name: "Hermes launch profile",
869
+ ok: false,
870
+ detail: error instanceof Error ? error.message : String(error),
871
+ });
872
+ }
873
+ }
657
874
  const failed = checks.filter((check) => !check.ok);
658
875
  console.log("ACE Doctor Report");
659
876
  console.log(`Workspace: ${WORKSPACE_ROOT}`);
@@ -740,11 +957,32 @@ async function main() {
740
957
  await startStdioServer();
741
958
  return;
742
959
  }
960
+ if (command === "mcp-shadow") {
961
+ const shadowArgs = args.slice(1);
962
+ const tools = (readFlagValue(shadowArgs, "--tools") ?? "")
963
+ .split(",")
964
+ .map((tool) => tool.trim())
965
+ .filter(Boolean);
966
+ await startHermesShadowStdioServer(tools);
967
+ return;
968
+ }
743
969
  if (command === "tui") {
744
970
  const tuiArgs = args.slice(1);
745
971
  const cliProvider = readFlagValue(tuiArgs, "--provider")?.trim() ||
746
972
  readFlagValue(tuiArgs, "--llm")?.trim();
747
973
  const cliModel = readFlagValue(tuiArgs, "--model")?.trim();
974
+ const cliEngine = readFlagValue(tuiArgs, "--engine")?.trim() ||
975
+ (tuiArgs.includes("--hermes") ? "hermes_local" : undefined);
976
+ const normalizedCliEngine = cliEngine?.toLowerCase().replace(/-/g, "_");
977
+ const hermesLaunchProfile = normalizedCliEngine === "hermes_local"
978
+ ? resolveHermesLaunchProfile({
979
+ workspaceRoot: WORKSPACE_ROOT,
980
+ cliHermesRoot: readFlagValue(tuiArgs, "--hermes-root"),
981
+ cliHermesPython: readFlagValue(tuiArgs, "--hermes-python"),
982
+ cliHermesUvProject: readFlagValue(tuiArgs, "--hermes-uv-project"),
983
+ cliHermesCommandJson: readFlagValue(tuiArgs, "--hermes-command-json"),
984
+ })
985
+ : undefined;
748
986
  const cliBaseUrl = readBaseUrlFlag(tuiArgs);
749
987
  const discovered = discoverProviderContext({
750
988
  workspaceRoot: WORKSPACE_ROOT,
@@ -774,8 +1012,50 @@ async function main() {
774
1012
  },
775
1013
  }
776
1014
  : discovered;
1015
+ // Merge any persisted discovery records from the workspace store so recent `ace doctor`
1016
+ // results are reflected immediately in the TUI startup options.
1017
+ try {
1018
+ const store = await openStore(getWorkspaceStorePath(WORKSPACE_ROOT), { readOnly: true });
1019
+ try {
1020
+ const discoveryRepo = new DiscoveryRepository(store);
1021
+ const stored = await discoveryRepo.listAll();
1022
+ const mergedProviders = new Set(resolvedTui.providers ?? []);
1023
+ const mergedModelsByProvider = { ...(resolvedTui.modelsByProvider ?? {}) };
1024
+ const mergedProviderBaseUrls = { ...(resolvedTui.providerBaseUrls ?? {}) };
1025
+ for (const p of stored) {
1026
+ if (!p || !p.provider)
1027
+ continue;
1028
+ const prov = String(p.provider).trim();
1029
+ if (!prov)
1030
+ continue;
1031
+ mergedProviders.add(prov);
1032
+ if (Array.isArray(p.models) && p.models.length > 0) {
1033
+ const existing = new Set(mergedModelsByProvider[prov] ?? []);
1034
+ for (const m of p.models)
1035
+ if (typeof m === "string" && m.trim())
1036
+ existing.add(m.trim());
1037
+ mergedModelsByProvider[prov] = [...existing].sort((a, b) => a.localeCompare(b));
1038
+ }
1039
+ if (p.endpoint && typeof p.endpoint === "string") {
1040
+ mergedProviderBaseUrls[prov] = p.endpoint;
1041
+ }
1042
+ }
1043
+ resolvedTui.providers = [...mergedProviders].sort((a, b) => a.localeCompare(b));
1044
+ resolvedTui.modelsByProvider = { ...(resolvedTui.modelsByProvider ?? {}), ...mergedModelsByProvider };
1045
+ resolvedTui.providerBaseUrls = { ...(resolvedTui.providerBaseUrls ?? {}), ...mergedProviderBaseUrls };
1046
+ }
1047
+ finally {
1048
+ await store.close();
1049
+ }
1050
+ }
1051
+ catch (err) {
1052
+ // Non-fatal — continue with discovered defaults if store read fails.
1053
+ console.warn(`Could not merge discovery records into TUI startup: ${err instanceof Error ? err.message : String(err)}`);
1054
+ }
777
1055
  await runTui({
778
1056
  provider: resolvedTui.provider,
1057
+ engine: cliEngine,
1058
+ hermesLaunchProfile,
779
1059
  model: resolvedTui.model,
780
1060
  providers: resolvedTui.providers,
781
1061
  modelsByProvider: resolvedTui.modelsByProvider,
@@ -26,6 +26,8 @@ export declare const ALL_MCP_CLIENTS: readonly ["codex", "vscode", "copilot", "c
26
26
  export type McpClient = (typeof ALL_MCP_CLIENTS)[number];
27
27
  export declare const ALL_LLM_PROVIDERS: readonly ["ollama", "llama.cpp", "codex", "claude", "gemini", "copilot"];
28
28
  export type LlmProvider = (typeof ALL_LLM_PROVIDERS)[number];
29
+ export declare const ALL_EXECUTION_ENGINES: readonly ["direct", "hermes_local"];
30
+ export type ExecutionEngine = (typeof ALL_EXECUTION_ENGINES)[number];
29
31
  export declare const ALL_AGENTS: readonly ["orchestrator", "vos", "ui", "coders", "astgrep", "skeptic", "ops", "research", "spec", "builder", "qa", "docs", "memory", "security", "observability", "eval", "release", "planner"];
30
32
  export type AgentRole = (typeof ALL_AGENTS)[number];
31
33
  export declare const SWARM_AGENTS: readonly ["orchestrator", "vos", "ui", "coders"];
@@ -53,6 +53,7 @@ export const ALL_LLM_PROVIDERS = [
53
53
  "gemini",
54
54
  "copilot",
55
55
  ];
56
+ export const ALL_EXECUTION_ENGINES = ["direct", "hermes_local"];
56
57
  export const ALL_AGENTS = [
57
58
  "orchestrator",
58
59
  "vos",
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ export declare const HERMES_BRIDGE_PROTOCOL_VERSION = "1.0.0";
3
+ declare const bridgeEventSchema: z.ZodObject<{
4
+ type: z.ZodEnum<{
5
+ error: "error";
6
+ status: "status";
7
+ tool_start: "tool_start";
8
+ session_open: "session_open";
9
+ turn_run: "turn_run";
10
+ interrupt: "interrupt";
11
+ session_close: "session_close";
12
+ delta: "delta";
13
+ reasoning: "reasoning";
14
+ assistant_interim: "assistant_interim";
15
+ tool_progress: "tool_progress";
16
+ tool_complete: "tool_complete";
17
+ final: "final";
18
+ }>;
19
+ session_id: z.ZodOptional<z.ZodString>;
20
+ turn_id: z.ZodOptional<z.ZodString>;
21
+ sequence: z.ZodOptional<z.ZodNumber>;
22
+ }, z.core.$loose>;
23
+ export type HermesBridgeEvent = z.infer<typeof bridgeEventSchema>;
24
+ export interface DecodedHermesBridgeFrame {
25
+ ok: true;
26
+ event: HermesBridgeEvent;
27
+ }
28
+ export interface RejectedHermesBridgeFrame {
29
+ ok: false;
30
+ reason: string;
31
+ raw: string;
32
+ }
33
+ export type HermesBridgeFrame = DecodedHermesBridgeFrame | RejectedHermesBridgeFrame;
34
+ export declare class HermesBridgeFrameDecoder {
35
+ private decoder;
36
+ private buffer;
37
+ push(chunk: Uint8Array, end?: boolean): HermesBridgeFrame[];
38
+ }
39
+ export declare function decodeHermesBridgeLine(line: string): HermesBridgeFrame;
40
+ export {};
41
+ //# sourceMappingURL=bridge-protocol.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { TextDecoder } from "node:util";
2
+ import { z } from "zod";
3
+ export const HERMES_BRIDGE_PROTOCOL_VERSION = "1.0.0";
4
+ const bridgeEventBaseSchema = z.object({
5
+ type: z.enum([
6
+ "session_open",
7
+ "turn_run",
8
+ "interrupt",
9
+ "session_close",
10
+ "status",
11
+ "delta",
12
+ "reasoning",
13
+ "assistant_interim",
14
+ "tool_start",
15
+ "tool_progress",
16
+ "tool_complete",
17
+ "final",
18
+ "error",
19
+ ]),
20
+ session_id: z.string().optional(),
21
+ turn_id: z.string().optional(),
22
+ sequence: z.number().int().nonnegative().optional(),
23
+ });
24
+ const bridgeEventSchema = bridgeEventBaseSchema.passthrough();
25
+ function sanitizeBridgeLine(line) {
26
+ if (line.includes("\u0000"))
27
+ return undefined;
28
+ const stripped = line.replace(/[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "");
29
+ const trimmed = stripped.trim();
30
+ return trimmed ? trimmed : undefined;
31
+ }
32
+ export class HermesBridgeFrameDecoder {
33
+ decoder = new TextDecoder("utf-8", { fatal: false });
34
+ buffer = "";
35
+ push(chunk, end = false) {
36
+ this.buffer += this.decoder.decode(chunk, { stream: !end });
37
+ const lines = this.buffer.split(/\r?\n/);
38
+ this.buffer = end ? "" : (lines.pop() ?? "");
39
+ const frames = [];
40
+ for (const line of lines) {
41
+ if (!line.trim())
42
+ continue;
43
+ frames.push(decodeHermesBridgeLine(line));
44
+ }
45
+ if (end && this.buffer.trim()) {
46
+ frames.push(decodeHermesBridgeLine(this.buffer));
47
+ this.buffer = "";
48
+ }
49
+ return frames;
50
+ }
51
+ }
52
+ export function decodeHermesBridgeLine(line) {
53
+ const sanitized = sanitizeBridgeLine(line);
54
+ if (!sanitized) {
55
+ return { ok: false, reason: "bridge_frame_control_bytes", raw: line };
56
+ }
57
+ try {
58
+ const parsed = JSON.parse(sanitized);
59
+ const event = bridgeEventSchema.parse(parsed);
60
+ return { ok: true, event };
61
+ }
62
+ catch (error) {
63
+ return {
64
+ ok: false,
65
+ reason: error instanceof Error ? error.message : "bridge_frame_invalid_json",
66
+ raw: sanitized,
67
+ };
68
+ }
69
+ }
70
+ //# sourceMappingURL=bridge-protocol.js.map
@@ -0,0 +1,19 @@
1
+ export interface HermesLaunchProfile {
2
+ enabled: boolean;
3
+ mode: "ephemeral_subprocess" | "external_command";
4
+ hermes_root?: string;
5
+ command: readonly string[];
6
+ worker_path: string;
7
+ source: "cli" | "env" | "store" | "default";
8
+ diagnostics: string[];
9
+ }
10
+ export interface ResolveHermesLaunchProfileInput {
11
+ workspaceRoot: string;
12
+ cliHermesRoot?: string;
13
+ cliHermesPython?: string;
14
+ cliHermesUvProject?: string;
15
+ cliHermesCommandJson?: string;
16
+ env?: NodeJS.ProcessEnv;
17
+ }
18
+ export declare function resolveHermesLaunchProfile(input: ResolveHermesLaunchProfileInput): HermesLaunchProfile;
19
+ //# sourceMappingURL=launch-profile.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readStoreJsonSync } from "../store/store-snapshot.js";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const DIST_ROOT = resolve(__dirname, "..");
8
+ const PACKAGE_ROOT = resolve(DIST_ROOT, "..");
9
+ function parseCommandJson(raw, source) {
10
+ if (!raw?.trim())
11
+ return undefined;
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(raw);
15
+ }
16
+ catch (error) {
17
+ throw new Error(`${source} must be a JSON array of command tokens.`);
18
+ }
19
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((item) => typeof item !== "string" || !item.trim())) {
20
+ throw new Error(`${source} must be a non-empty JSON array of non-empty strings.`);
21
+ }
22
+ return parsed.map((item) => item.trim());
23
+ }
24
+ function defaultHermesRoot() {
25
+ const candidate = resolve(PACKAGE_ROOT, "..", ".tmp", "hermes-agent");
26
+ return existsSync(candidate) ? candidate : undefined;
27
+ }
28
+ export function resolveHermesLaunchProfile(input) {
29
+ const env = input.env ?? process.env;
30
+ const diagnostics = [];
31
+ const workerPath = resolve(PACKAGE_ROOT, "scripts", "hermes_bridge_worker.py");
32
+ const stored = readStoreJsonSync(input.workspaceRoot, "state/runtime/hermes_launch_profile") ??
33
+ undefined;
34
+ const hermesRoot = input.cliHermesRoot ?? env.ACE_HERMES_ROOT ?? stored?.hermes_root ?? defaultHermesRoot();
35
+ const commandJson = input.cliHermesCommandJson ?? env.ACE_HERMES_COMMAND_JSON ?? stored?.hermes_command_json;
36
+ const uvProject = input.cliHermesUvProject ?? env.ACE_HERMES_UV_PROJECT ?? stored?.hermes_uv_project;
37
+ const python = input.cliHermesPython ?? env.HERMES_PYTHON ?? stored?.hermes_python;
38
+ let command;
39
+ let source = "default";
40
+ if (commandJson) {
41
+ command = parseCommandJson(commandJson, input.cliHermesCommandJson ? "--hermes-command-json" : "ACE_HERMES_COMMAND_JSON");
42
+ source = input.cliHermesCommandJson ? "cli" : "env";
43
+ if (!input.cliHermesCommandJson && !env.ACE_HERMES_COMMAND_JSON) {
44
+ source = "store";
45
+ }
46
+ }
47
+ else if (uvProject) {
48
+ command = ["uv", "run", "--project", uvProject, "python"];
49
+ source = input.cliHermesUvProject ? "cli" : "env";
50
+ if (!input.cliHermesUvProject && !env.ACE_HERMES_UV_PROJECT) {
51
+ source = "store";
52
+ }
53
+ }
54
+ else if (python) {
55
+ command = [python];
56
+ source = input.cliHermesPython ? "cli" : "env";
57
+ if (!input.cliHermesPython && !env.HERMES_PYTHON) {
58
+ source = "store";
59
+ }
60
+ }
61
+ else {
62
+ command = ["python3"];
63
+ diagnostics.push("HERMES_PYTHON not set; using python3 fallback.");
64
+ }
65
+ if (!hermesRoot) {
66
+ diagnostics.push("ACE_HERMES_ROOT not set and no default Hermes checkout was found.");
67
+ }
68
+ if (!existsSync(workerPath)) {
69
+ diagnostics.push(`Hermes bridge worker is missing at ${workerPath}.`);
70
+ }
71
+ return {
72
+ enabled: true,
73
+ mode: commandJson ? "external_command" : "ephemeral_subprocess",
74
+ hermes_root: hermesRoot,
75
+ command,
76
+ worker_path: workerPath,
77
+ source,
78
+ diagnostics,
79
+ };
80
+ }
81
+ //# sourceMappingURL=launch-profile.js.map
@@ -0,0 +1,42 @@
1
+ import type { BridgeResult } from "../model-bridge.js";
2
+ import type { LocalModelRuntimeConfig } from "../local-model-runtime.js";
3
+ import type { LocalModelExecutionPolicy } from "../local-model-policy.js";
4
+ import { type HermesBridgeEvent } from "./bridge-protocol.js";
5
+ import { type HermesLaunchProfile } from "./launch-profile.js";
6
+ export interface HermesLocalTurnOptions {
7
+ task: string;
8
+ role: string;
9
+ runtime: LocalModelRuntimeConfig;
10
+ policy: LocalModelExecutionPolicy;
11
+ toolScope?: string[];
12
+ maxTurns: number;
13
+ }
14
+ export interface HermesLocalTurnResult {
15
+ result: BridgeResult;
16
+ metadata: {
17
+ bridge_protocol_version: string;
18
+ hermes_session_id: string;
19
+ shadow_mcp_session_id: string;
20
+ execution_engine: "hermes_local";
21
+ provider: string;
22
+ model: string;
23
+ events: HermesBridgeEvent[];
24
+ };
25
+ }
26
+ export interface HermesLocalExecutor {
27
+ runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
28
+ }
29
+ export interface HermesSubprocessExecutorOptions {
30
+ hermesRoot?: string;
31
+ workerPath?: string;
32
+ pythonCommand?: string;
33
+ command?: readonly string[];
34
+ launchProfile?: HermesLaunchProfile;
35
+ keepTemp?: boolean;
36
+ }
37
+ export declare class HermesSubprocessExecutor implements HermesLocalExecutor {
38
+ private options;
39
+ constructor(options?: HermesSubprocessExecutorOptions);
40
+ runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
41
+ }
42
+ //# sourceMappingURL=session-manager.d.ts.map