aos-harness 0.4.1 → 0.5.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.
@@ -1,12 +1,23 @@
1
1
  {
2
2
  "name": "@aos-harness/claude-code-adapter",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
+ "description": "AOS Harness adapter for Anthropic's Claude Code CLI.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/aos-engineer/aos-harness.git",
9
+ "directory": "adapters/claude-code"
10
+ },
11
+ "homepage": "https://aos.engineer",
12
+ "keywords": ["aos-harness", "ai-agents", "claude-code", "adapter"],
4
13
  "type": "module",
14
+ "engines": { "bun": ">=1.0.0" },
5
15
  "exports": { ".": "./src/index.ts" },
6
- "files": ["src/"],
16
+ "files": ["src/", "README.md"],
17
+ "publishConfig": { "access": "public" },
7
18
  "dependencies": {
8
- "@aos-harness/runtime": "0.4.1",
9
- "@aos-harness/adapter-shared": "0.4.1"
19
+ "@aos-harness/runtime": "0.5.0",
20
+ "@aos-harness/adapter-shared": "0.5.0"
10
21
  },
11
22
  "devDependencies": {
12
23
  "@types/bun": "latest",
@@ -17,6 +17,13 @@ import {
17
17
  } from "@aos-harness/adapter-shared";
18
18
  import type { BaseEventBus } from "@aos-harness/adapter-shared";
19
19
 
20
+ // ── McpBridgeOptions ─────────────────────────────────────────────
21
+
22
+ export interface McpBridgeOptions {
23
+ bridgeScriptPath: string;
24
+ socketPath: string;
25
+ }
26
+
20
27
  // ── ClaudeCodeAgentRuntime ────────────────────────────────────────
21
28
 
22
29
  export class ClaudeCodeAgentRuntime extends BaseAgentRuntime {
@@ -57,6 +64,9 @@ export class ClaudeCodeAgentRuntime extends BaseAgentRuntime {
57
64
  args.push("--resume", state.sessionFile);
58
65
  }
59
66
 
67
+ // Extra args (e.g. MCP flags) are spliced in before the positional message
68
+ args.push(...(opts?.extraArgs ?? []));
69
+
60
70
  // Message is always the final argument
61
71
  args.push(message);
62
72
  return args;
@@ -179,4 +189,22 @@ export class ClaudeCodeAgentRuntime extends BaseAgentRuntime {
179
189
  };
180
190
  return pricing[tier];
181
191
  }
192
+
193
+ buildMcpArgs(opts: McpBridgeOptions): string[] {
194
+ const config = JSON.stringify({
195
+ mcpServers: {
196
+ aos: {
197
+ command: "bun",
198
+ args: [opts.bridgeScriptPath],
199
+ env: { AOS_BRIDGE_SOCKET: opts.socketPath },
200
+ },
201
+ },
202
+ });
203
+ return [
204
+ "--mcp-config", config,
205
+ "--strict-mcp-config",
206
+ "--allowedTools", "mcp__aos__delegate mcp__aos__end",
207
+ "--permission-mode", "bypassPermissions",
208
+ ];
209
+ }
182
210
  }
@@ -1,2 +1,2 @@
1
- export { ClaudeCodeAgentRuntime } from "./agent-runtime";
1
+ export { ClaudeCodeAgentRuntime, type McpBridgeOptions } from "./agent-runtime";
2
2
  export { BaseEventBus, TerminalUI, BaseWorkflow, composeAdapter } from "@aos-harness/adapter-shared";
@@ -0,0 +1,14 @@
1
+ # @aos-harness/codex-adapter
2
+
3
+ AOS Harness adapter for OpenAI's [Codex CLI](https://github.com/openai/codex). Lets you run AOS deliberation and execution profiles with Codex as the underlying agent runtime.
4
+
5
+ Part of the [AOS Harness](https://aos.engineer) monorepo. Bundled with the `aos-harness` CLI — install standalone only if you want a lean, Codex-only setup.
6
+
7
+ ## Requirements
8
+
9
+ - Bun ≥ 1.0.0
10
+ - Codex CLI installed and authenticated on the host
11
+
12
+ ## License
13
+
14
+ MIT
@@ -1,12 +1,23 @@
1
1
  {
2
2
  "name": "@aos-harness/codex-adapter",
3
- "version": "0.1.0",
3
+ "version": "0.5.0",
4
+ "description": "AOS Harness adapter for OpenAI's Codex CLI.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/aos-engineer/aos-harness.git",
9
+ "directory": "adapters/codex"
10
+ },
11
+ "homepage": "https://aos.engineer",
12
+ "keywords": ["aos-harness", "ai-agents", "codex", "adapter"],
4
13
  "type": "module",
14
+ "engines": { "bun": ">=1.0.0" },
5
15
  "exports": { ".": "./src/index.ts" },
6
- "files": ["src/"],
16
+ "files": ["src/", "README.md"],
17
+ "publishConfig": { "access": "public" },
7
18
  "dependencies": {
8
- "@aos-harness/runtime": "0.4.1",
9
- "@aos-harness/adapter-shared": "0.4.1"
19
+ "@aos-harness/runtime": "0.5.0",
20
+ "@aos-harness/adapter-shared": "0.5.0"
10
21
  },
11
22
  "devDependencies": {
12
23
  "@types/bun": "latest",
@@ -17,6 +17,13 @@ import {
17
17
  } from "@aos-harness/adapter-shared";
18
18
  import type { BaseEventBus } from "@aos-harness/adapter-shared";
19
19
 
20
+ // ── McpBridgeOptions ──────────────────────────────────────────────
21
+
22
+ export interface McpBridgeOptions {
23
+ bridgeScriptPath: string;
24
+ socketPath: string;
25
+ }
26
+
20
27
  // ── CodexAgentRuntime ─────────────────────────────────────────────
21
28
 
22
29
  export class CodexAgentRuntime extends BaseAgentRuntime {
@@ -54,6 +61,9 @@ export class CodexAgentRuntime extends BaseAgentRuntime {
54
61
  args.push("--session", state.sessionFile);
55
62
  }
56
63
 
64
+ // Extra args (e.g. MCP flags) are spliced in before the positional message
65
+ args.push(...(opts?.extraArgs ?? []));
66
+
57
67
  // Message is always the final argument
58
68
  args.push(message);
59
69
  return args;
@@ -174,6 +184,18 @@ export class CodexAgentRuntime extends BaseAgentRuntime {
174
184
  return { type: "unknown", metered: false };
175
185
  }
176
186
 
187
+ // Paths are controlled by adapter-session.ts (temp sock in /tmp, script in installed package) — shell-safe characters guaranteed; no escaping needed.
188
+ buildMcpArgs(opts: McpBridgeOptions): string[] {
189
+ return [
190
+ "-c", `mcp_servers.aos.command="bun"`,
191
+ "-c", `mcp_servers.aos.args=["${opts.bridgeScriptPath}"]`,
192
+ "-c", `mcp_servers.aos.env={AOS_BRIDGE_SOCKET="${opts.socketPath}"}`,
193
+ "-c", `mcp_servers.aos.required=true`,
194
+ "-c", `mcp_servers.aos.enabled_tools=["delegate","end"]`,
195
+ "-c", `mcp_servers.aos.tool_timeout_sec=600`,
196
+ ];
197
+ }
198
+
177
199
  getModelCost(tier: ModelTier): ModelCost {
178
200
  const pricing: Record<ModelTier, ModelCost> = {
179
201
  economy: {
@@ -1,12 +1,23 @@
1
1
  {
2
2
  "name": "@aos-harness/gemini-adapter",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
+ "description": "AOS Harness adapter for Google's Gemini CLI.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/aos-engineer/aos-harness.git",
9
+ "directory": "adapters/gemini"
10
+ },
11
+ "homepage": "https://aos.engineer",
12
+ "keywords": ["aos-harness", "ai-agents", "gemini", "adapter"],
4
13
  "type": "module",
14
+ "engines": { "bun": ">=1.0.0" },
5
15
  "exports": { ".": "./src/index.ts" },
6
- "files": ["src/"],
16
+ "files": ["src/", "README.md"],
17
+ "publishConfig": { "access": "public" },
7
18
  "dependencies": {
8
- "@aos-harness/runtime": "0.4.1",
9
- "@aos-harness/adapter-shared": "0.4.1"
19
+ "@aos-harness/runtime": "0.5.0",
20
+ "@aos-harness/adapter-shared": "0.5.0"
10
21
  },
11
22
  "devDependencies": {
12
23
  "@types/bun": "latest",
@@ -2,6 +2,11 @@
2
2
  // Extends BaseAgentRuntime with Gemini CLI integration.
3
3
 
4
4
  import { execSync } from "node:child_process";
5
+
6
+ export interface McpBridgeOptions {
7
+ bridgeScriptPath: string;
8
+ socketPath: string;
9
+ }
5
10
  import type {
6
11
  AuthMode,
7
12
  ModelCost,
@@ -54,6 +59,9 @@ export class GeminiAgentRuntime extends BaseAgentRuntime {
54
59
  args.push("--session", state.sessionFile);
55
60
  }
56
61
 
62
+ // Extra args (e.g. MCP flags) are spliced in before the positional message
63
+ args.push(...(opts?.extraArgs ?? []));
64
+
57
65
  // Message is always the final argument
58
66
  args.push(message);
59
67
  return args;
@@ -190,4 +198,48 @@ export class GeminiAgentRuntime extends BaseAgentRuntime {
190
198
  };
191
199
  return pricing[tier];
192
200
  }
201
+
202
+ buildMcpArgs(): string[] {
203
+ return [
204
+ "--yolo",
205
+ "--allowed-mcp-server-names", "aos",
206
+ ];
207
+ }
208
+
209
+ /**
210
+ * Writes a project-local .gemini/settings.json with our MCP server config.
211
+ * Returns a restore function that the caller MUST invoke on shutdown to
212
+ * restore any pre-existing settings file.
213
+ */
214
+ writeMcpSettings(opts: McpBridgeOptions & { projectRoot: string }): () => void {
215
+ const { mkdirSync, writeFileSync, existsSync, renameSync, unlinkSync } = require("node:fs") as typeof import("node:fs");
216
+ const { join } = require("node:path") as typeof import("node:path");
217
+
218
+ const geminiDir = join(opts.projectRoot, ".gemini");
219
+ const settingsPath = join(geminiDir, "settings.json");
220
+ const backupPath = join(geminiDir, "settings.json.aos-backup");
221
+
222
+ mkdirSync(geminiDir, { recursive: true });
223
+ const hadBackup = existsSync(settingsPath);
224
+ if (hadBackup) renameSync(settingsPath, backupPath);
225
+
226
+ writeFileSync(settingsPath, JSON.stringify({
227
+ mcpServers: {
228
+ aos: {
229
+ command: "bun",
230
+ args: [opts.bridgeScriptPath],
231
+ env: { AOS_BRIDGE_SOCKET: opts.socketPath },
232
+ trust: true,
233
+ timeout: 600000,
234
+ },
235
+ },
236
+ }, null, 2));
237
+
238
+ return () => {
239
+ try { unlinkSync(settingsPath); } catch { /* ignore */ }
240
+ if (hadBackup) {
241
+ try { renameSync(backupPath, settingsPath); } catch { /* ignore */ }
242
+ }
243
+ };
244
+ }
193
245
  }
@@ -1,2 +1,2 @@
1
- export { GeminiAgentRuntime } from "./agent-runtime";
1
+ export { GeminiAgentRuntime, type McpBridgeOptions } from "./agent-runtime";
2
2
  export { BaseEventBus, TerminalUI, BaseWorkflow, composeAdapter } from "@aos-harness/adapter-shared";
@@ -1,15 +1,26 @@
1
1
  {
2
2
  "name": "@aos-harness/pi-adapter",
3
- "version": "0.1.0",
3
+ "version": "0.5.0",
4
+ "description": "AOS Harness adapter for the Pi coding agent runtime.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/aos-engineer/aos-harness.git",
9
+ "directory": "adapters/pi"
10
+ },
11
+ "homepage": "https://aos.engineer",
12
+ "keywords": ["aos-harness", "ai-agents", "pi", "adapter"],
4
13
  "type": "module",
14
+ "engines": { "bun": ">=1.0.0" },
5
15
  "pi": {
6
16
  "extensions": ["./src/index.ts"]
7
17
  },
8
18
  "exports": { ".": "./src/index.ts" },
9
- "files": ["src/"],
19
+ "files": ["src/", "README.md"],
20
+ "publishConfig": { "access": "public" },
10
21
  "dependencies": {
11
- "@aos-harness/adapter-shared": "0.4.1",
12
- "@aos-harness/runtime": "0.4.1",
22
+ "@aos-harness/adapter-shared": "0.5.0",
23
+ "@aos-harness/runtime": "0.5.0",
13
24
  "js-yaml": "^4.1.0"
14
25
  },
15
26
  "devDependencies": {
@@ -2,9 +2,9 @@
2
2
  // Wires all 4 adapter layers together and makes the AOS Harness
3
3
  // runnable as a Pi extension.
4
4
 
5
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync, symlinkSync, rmSync } from "node:fs";
5
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
6
6
  import { randomUUID } from "node:crypto";
7
- import { join, dirname, basename, resolve } from "node:path";
7
+ import { join, dirname, basename } from "node:path";
8
8
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
9
  import { Text, truncateToWidth } from "@mariozechner/pi-tui";
10
10
  import { Type } from "@sinclair/typebox";
@@ -12,8 +12,7 @@ import { Type } from "@sinclair/typebox";
12
12
  import { PiAgentRuntime } from "./agent-runtime";
13
13
  import { PiEventBus } from "./event-bus";
14
14
  import { PiUI } from "./ui";
15
- import { BaseWorkflow } from "@aos-harness/adapter-shared";
16
- import { composeAdapter } from "@aos-harness/adapter-shared";
15
+ import { BaseWorkflow, composeAdapter, discoverAgents, createFlatAgentsDir, findProjectRoot } from "@aos-harness/adapter-shared";
17
16
 
18
17
  import { AOSEngine } from "@aos-harness/runtime";
19
18
  import type { AOSAdapter, ConstraintState, ProfileConfig } from "@aos-harness/runtime/types";
@@ -22,75 +21,6 @@ import { validateBrief } from "@aos-harness/runtime/config-loader";
22
21
 
23
22
  // ── Helpers ─────────────────────────────────────────────────────
24
23
 
25
- /** Walk up from `cwd` looking for a directory containing `core/`. */
26
- function findProjectRoot(cwd: string): string | null {
27
- let dir = resolve(cwd);
28
- for (let i = 0; i < 20; i++) {
29
- if (existsSync(join(dir, "core"))) return dir;
30
- if (existsSync(join(dir, ".aos"))) return dir;
31
- const parent = dirname(dir);
32
- if (parent === dir) break;
33
- dir = parent;
34
- }
35
- return null;
36
- }
37
-
38
- /**
39
- * Recursively discover all agent directories (those containing agent.yaml).
40
- * Returns a Map of agentId -> absolute directory path.
41
- */
42
- function discoverAgents(agentsDir: string): Map<string, string> {
43
- const agents = new Map<string, string>();
44
-
45
- function walk(dir: string): void {
46
- if (!existsSync(dir)) return;
47
- const entries = readdirSync(dir, { withFileTypes: true });
48
- for (const entry of entries) {
49
- if (!entry.isDirectory()) continue;
50
- const subDir = join(dir, entry.name);
51
- const yamlPath = join(subDir, "agent.yaml");
52
- if (existsSync(yamlPath)) {
53
- // Read the id from agent.yaml
54
- try {
55
- const raw = readFileSync(yamlPath, "utf-8");
56
- const idMatch = raw.match(/^id:\s*(.+)$/m);
57
- if (idMatch) {
58
- agents.set(idMatch[1].trim(), subDir);
59
- }
60
- } catch {
61
- // Skip unreadable
62
- }
63
- }
64
- // Recurse into subdirectories
65
- walk(subDir);
66
- }
67
- }
68
-
69
- walk(agentsDir);
70
- return agents;
71
- }
72
-
73
- /**
74
- * Create a flat temporary directory with symlinks so the engine can
75
- * resolve agent IDs via `join(agentsDir, id)`.
76
- */
77
- function createFlatAgentsDir(projectRoot: string, agentMap: Map<string, string>): string {
78
- const flatDir = join(projectRoot, ".aos", "_flat_agents");
79
- if (existsSync(flatDir)) {
80
- rmSync(flatDir, { recursive: true, force: true });
81
- }
82
- mkdirSync(flatDir, { recursive: true });
83
-
84
- for (const [id, dirPath] of agentMap) {
85
- const linkPath = join(flatDir, id);
86
- if (!existsSync(linkPath)) {
87
- symlinkSync(dirPath, linkPath, "dir");
88
- }
89
- }
90
-
91
- return flatDir;
92
- }
93
-
94
24
  /** List subdirectories that contain a given file. */
95
25
  function listDirsWithFile(parentDir: string, fileName: string): { name: string; dir: string; mtime: number }[] {
96
26
  if (!existsSync(parentDir)) return [];
@@ -0,0 +1,15 @@
1
+ # @aos-harness/adapter-shared
2
+
3
+ Shared base classes and utilities used by every AOS Harness platform adapter (Claude Code, Codex, Gemini, Pi).
4
+
5
+ Re-exports common types (`AgentRuntime`, `EventBus`, etc.) and provides composition helpers (`composeAdapter`, `BaseEventBus`, `BaseWorkflow`) so each adapter only needs to implement platform-specific runtime logic.
6
+
7
+ Part of the [AOS Harness](https://aos.engineer) monorepo. Most users install `aos-harness` (the CLI) instead of consuming this package directly.
8
+
9
+ ## Requirements
10
+
11
+ - Bun ≥ 1.0.0
12
+
13
+ ## License
14
+
15
+ MIT
@@ -1,14 +1,25 @@
1
1
  {
2
2
  "name": "@aos-harness/adapter-shared",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
+ "description": "Shared base classes and utilities for AOS Harness platform adapters.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/aos-engineer/aos-harness.git",
9
+ "directory": "adapters/shared"
10
+ },
11
+ "homepage": "https://aos.engineer",
12
+ "keywords": ["aos-harness", "ai-agents", "adapter", "shared"],
4
13
  "type": "module",
14
+ "engines": { "bun": ">=1.0.0" },
5
15
  "exports": {
6
16
  ".": "./src/index.ts",
7
17
  "./*": "./src/*"
8
18
  },
9
- "files": ["src/"],
19
+ "files": ["src/", "README.md"],
20
+ "publishConfig": { "access": "public" },
10
21
  "dependencies": {
11
- "@aos-harness/runtime": "0.4.1",
22
+ "@aos-harness/runtime": "0.5.0",
12
23
  "js-yaml": "^4.1.0"
13
24
  },
14
25
  "devDependencies": {
@@ -0,0 +1,71 @@
1
+ import { existsSync, readdirSync, readFileSync, mkdirSync, symlinkSync, rmSync } from "node:fs";
2
+ import { join, dirname, resolve } from "node:path";
3
+
4
+ /** Walk up from `cwd` looking for a directory containing `core/`. */
5
+ export function findProjectRoot(cwd: string): string | null {
6
+ let dir = resolve(cwd);
7
+ for (let i = 0; i < 20; i++) {
8
+ if (existsSync(join(dir, "core"))) return dir;
9
+ if (existsSync(join(dir, ".aos"))) return dir;
10
+ const parent = dirname(dir);
11
+ if (parent === dir) break;
12
+ dir = parent;
13
+ }
14
+ return null;
15
+ }
16
+
17
+ /**
18
+ * Recursively discover all agent directories (those containing agent.yaml).
19
+ * Returns a Map of agentId -> absolute directory path.
20
+ */
21
+ export function discoverAgents(agentsDir: string): Map<string, string> {
22
+ const agents = new Map<string, string>();
23
+
24
+ function walk(dir: string): void {
25
+ if (!existsSync(dir)) return;
26
+ const entries = readdirSync(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (!entry.isDirectory()) continue;
29
+ const subDir = join(dir, entry.name);
30
+ const yamlPath = join(subDir, "agent.yaml");
31
+ if (existsSync(yamlPath)) {
32
+ // Read the id from agent.yaml
33
+ try {
34
+ const raw = readFileSync(yamlPath, "utf-8");
35
+ const idMatch = raw.match(/^id:\s*(.+)$/m);
36
+ if (idMatch) {
37
+ agents.set(idMatch[1].trim(), subDir);
38
+ }
39
+ } catch {
40
+ // Skip unreadable
41
+ }
42
+ }
43
+ // Recurse into subdirectories
44
+ walk(subDir);
45
+ }
46
+ }
47
+
48
+ walk(agentsDir);
49
+ return agents;
50
+ }
51
+
52
+ /**
53
+ * Create a flat temporary directory with symlinks so the engine can
54
+ * resolve agent IDs via `join(agentsDir, id)`.
55
+ */
56
+ export function createFlatAgentsDir(projectRoot: string, agentMap: Map<string, string>): string {
57
+ const flatDir = join(projectRoot, ".aos", "_flat_agents");
58
+ if (existsSync(flatDir)) {
59
+ rmSync(flatDir, { recursive: true, force: true });
60
+ }
61
+ mkdirSync(flatDir, { recursive: true });
62
+
63
+ for (const [id, dirPath] of agentMap) {
64
+ const linkPath = join(flatDir, id);
65
+ if (!existsSync(linkPath)) {
66
+ symlinkSync(dirPath, linkPath, "dir");
67
+ }
68
+ }
69
+
70
+ return flatDir;
71
+ }
@@ -9,3 +9,4 @@ export { BaseEventBus } from "./base-event-bus";
9
9
  export { TerminalUI } from "./terminal-ui";
10
10
  export { BaseWorkflow } from "./base-workflow";
11
11
  export { composeAdapter } from "./compose";
12
+ export { discoverAgents, createFlatAgentsDir, findProjectRoot } from "./agent-discovery";
@@ -0,0 +1,65 @@
1
+ import { test, expect } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, lstatSync, realpathSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { discoverAgents, findProjectRoot, createFlatAgentsDir } from "../src/agent-discovery";
6
+
7
+ test("discoverAgents finds agents recursively by agent.yaml", () => {
8
+ const root = mkdtempSync(join(tmpdir(), "discover-"));
9
+ mkdirSync(join(root, "alice"), { recursive: true });
10
+ writeFileSync(join(root, "alice", "agent.yaml"), "id: alice\n");
11
+ mkdirSync(join(root, "nested", "bob"), { recursive: true });
12
+ writeFileSync(join(root, "nested", "bob", "agent.yaml"), "id: bob\n");
13
+
14
+ const map = discoverAgents(root);
15
+ expect(map.get("alice")).toBe(join(root, "alice"));
16
+ expect(map.get("bob")).toBe(join(root, "nested", "bob"));
17
+ });
18
+
19
+ test("findProjectRoot walks up to find core/ or .aos/", () => {
20
+ const root = mkdtempSync(join(tmpdir(), "proj-"));
21
+ mkdirSync(join(root, "core"));
22
+ const deep = join(root, "a", "b", "c");
23
+ mkdirSync(deep, { recursive: true });
24
+ expect(findProjectRoot(deep)).toBe(root);
25
+ });
26
+
27
+ test("createFlatAgentsDir creates symlinks and wipes on re-call", () => {
28
+ // Set up a projectRoot temp dir
29
+ const projectRoot = mkdtempSync(join(tmpdir(), "flat-agents-"));
30
+
31
+ // Create two agent dirs
32
+ const agentAliceDir = mkdtempSync(join(tmpdir(), "agent-alice-"));
33
+ const agentBobDir = mkdtempSync(join(tmpdir(), "agent-bob-"));
34
+
35
+ const agentMap = new Map<string, string>([
36
+ ["alice", agentAliceDir],
37
+ ["bob", agentBobDir],
38
+ ]);
39
+
40
+ // First call
41
+ const flatDir = createFlatAgentsDir(projectRoot, agentMap);
42
+
43
+ // Assert returned path
44
+ expect(flatDir).toBe(join(projectRoot, ".aos", "_flat_agents"));
45
+
46
+ // Assert symlinks exist and resolve correctly
47
+ const aliceLink = join(flatDir, "alice");
48
+ const bobLink = join(flatDir, "bob");
49
+
50
+ expect(lstatSync(aliceLink).isSymbolicLink()).toBe(true);
51
+ expect(lstatSync(bobLink).isSymbolicLink()).toBe(true);
52
+ expect(realpathSync(aliceLink)).toBe(realpathSync(agentAliceDir));
53
+ expect(realpathSync(bobLink)).toBe(realpathSync(agentBobDir));
54
+
55
+ // Second call with only bob — alice should be gone (dir wiped+recreated)
56
+ const agentMapReduced = new Map<string, string>([["bob", agentBobDir]]);
57
+ createFlatAgentsDir(projectRoot, agentMapReduced);
58
+
59
+ // alice symlink from the first call must no longer exist
60
+ expect(() => lstatSync(aliceLink)).toThrow();
61
+
62
+ // bob symlink must still be present and valid
63
+ expect(lstatSync(bobLink).isSymbolicLink()).toBe(true);
64
+ expect(realpathSync(bobLink)).toBe(realpathSync(agentBobDir));
65
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aos-harness",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Agentic Orchestration System — assemble AI agents into deliberation and execution teams",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,7 +36,9 @@
36
36
  "test": "bun run src/index.ts validate"
37
37
  },
38
38
  "dependencies": {
39
- "@aos-harness/runtime": "0.4.1",
39
+ "@aos-harness/adapter-shared": "0.5.0",
40
+ "@aos-harness/runtime": "0.5.0",
41
+ "@modelcontextprotocol/sdk": "^1.29.0",
40
42
  "js-yaml": "^4.1.0"
41
43
  },
42
44
  "devDependencies": {
@@ -0,0 +1,16 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import yaml from "js-yaml";
4
+
5
+ export interface AdapterConfig {
6
+ platform?: string;
7
+ model_overrides?: Partial<Record<string, string>>;
8
+ theme?: string;
9
+ editor?: string;
10
+ }
11
+
12
+ export function readAdapterConfig(root: string): AdapterConfig | null {
13
+ const p = join(root, ".aos", "adapter.yaml");
14
+ if (!existsSync(p)) return null;
15
+ return yaml.load(readFileSync(p, "utf-8")) as AdapterConfig;
16
+ }
@@ -0,0 +1,319 @@
1
+ // ── adapter-session.ts ────────────────────────────────────────────
2
+ // Orchestration entrypoint used by the CLI to run a deliberation
3
+ // session against one of the non-Pi adapters (Claude Code, Gemini, Codex).
4
+ //
5
+ // Responsibilities:
6
+ // 1. Dynamically load the chosen AgentRuntime class.
7
+ // 2. Compose the 4 adapter layers into an AOSAdapter.
8
+ // 3. Build the AOSEngine and kick off with the brief.
9
+ // 4. Start a Unix-socket bridge that forwards MCP `delegate`/`end`
10
+ // tool calls from the arbiter process into the engine.
11
+ // 5. Resolve the arbiter prompt template and spawn the arbiter,
12
+ // threading MCP CLI flags through MessageOpts.extraArgs.
13
+ // 6. Run a readline loop for `/aos-*` interactive commands.
14
+
15
+ import { join, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { tmpdir } from "node:os";
18
+ import { readFileSync } from "node:fs";
19
+ import readline from "node:readline";
20
+ import {
21
+ BaseEventBus,
22
+ TerminalUI,
23
+ BaseWorkflow,
24
+ composeAdapter,
25
+ discoverAgents,
26
+ createFlatAgentsDir,
27
+ } from "@aos-harness/adapter-shared";
28
+ import { AOSEngine } from "@aos-harness/runtime";
29
+ import { loadAgent } from "@aos-harness/runtime/config-loader";
30
+ import { resolveTemplate } from "@aos-harness/runtime/template-resolver";
31
+ import { startBridgeServer } from "./bridge-server";
32
+ import { renderTextGauge, renderRoundOneLiner } from "./gauges";
33
+
34
+ export interface AdapterSessionConfig {
35
+ platform: string; // "claude-code" | "gemini" | "codex"
36
+ profileDir: string;
37
+ briefPath: string;
38
+ domainName: string | null;
39
+ root: string;
40
+ sessionId: string;
41
+ deliberationDir: string;
42
+ verbose: boolean;
43
+ workflowConfig: any | null;
44
+ workflowsDir: string;
45
+ modelOverrides?: Partial<Record<string, string>>;
46
+ }
47
+
48
+ const ADAPTER_MAP: Record<string, { package: string; className: string }> = {
49
+ "claude-code": {
50
+ package: "@aos-harness/claude-code-adapter",
51
+ className: "ClaudeCodeAgentRuntime",
52
+ },
53
+ gemini: {
54
+ package: "@aos-harness/gemini-adapter",
55
+ className: "GeminiAgentRuntime",
56
+ },
57
+ codex: {
58
+ package: "@aos-harness/codex-adapter",
59
+ className: "CodexAgentRuntime",
60
+ },
61
+ };
62
+
63
+ async function loadAdapterRuntime(platform: string): Promise<any> {
64
+ const entry = ADAPTER_MAP[platform];
65
+ if (!entry) throw new Error(`Unknown adapter: ${platform}`);
66
+
67
+ async function readAdapterVersion(fromPath: string): Promise<string> {
68
+ try {
69
+ const pkgUrl = new URL("../package.json", fromPath).href;
70
+ const { readFile } = await import("node:fs/promises");
71
+ const raw = await readFile(new URL(pkgUrl), "utf-8");
72
+ return (JSON.parse(raw) as { version: string }).version ?? "unknown";
73
+ } catch {
74
+ return "unknown";
75
+ }
76
+ }
77
+
78
+ try {
79
+ const mod = await import(entry.package);
80
+ const resolved = (import.meta as any).resolve?.(entry.package) ?? entry.package;
81
+ const version = await readAdapterVersion(resolved);
82
+ console.error(`[adapter] loaded ${entry.package}@${version} (standalone)`);
83
+ return mod[entry.className];
84
+ } catch {
85
+ const here = dirname(fileURLToPath(import.meta.url));
86
+ const fallback = join(here, "..", "..", "adapters", platform, "src", "index.ts");
87
+ const mod = await import(fallback);
88
+ const version = await readAdapterVersion(`file://${fallback}`);
89
+ console.error(`[adapter] loaded ${entry.package}@${version} (bundled: ${fallback})`);
90
+ return mod[entry.className];
91
+ }
92
+ }
93
+
94
+ function toolNamesForPlatform(platform: string): { delegate: string; end: string } {
95
+ if (platform === "claude-code") {
96
+ return { delegate: "mcp__aos__delegate", end: "mcp__aos__end" };
97
+ }
98
+ return { delegate: "delegate", end: "end" };
99
+ }
100
+
101
+ export async function runAdapterSession(config: AdapterSessionConfig): Promise<void> {
102
+ const log = (msg: string) => {
103
+ if (config.verbose) console.error(`[session] ${msg}`);
104
+ };
105
+
106
+ log("loading adapter runtime");
107
+ const RuntimeClass = await loadAdapterRuntime(config.platform);
108
+
109
+ // ── Layer composition ──────────────────────────────────────
110
+ const eventBus = new BaseEventBus();
111
+ const agentRuntime = new RuntimeClass(eventBus, config.modelOverrides);
112
+ const ui = new TerminalUI();
113
+ const workflow = new BaseWorkflow(agentRuntime, config.root);
114
+ const adapter = composeAdapter(agentRuntime, eventBus, ui, workflow);
115
+ log("layers composed");
116
+
117
+ // ── Agent discovery (flatten nested core/agents layout) ───
118
+ const agentsDir = join(config.root, "core", "agents");
119
+ const agentMap = discoverAgents(agentsDir);
120
+ const flatAgentsDir = createFlatAgentsDir(config.root, agentMap);
121
+ const domainsDir = join(config.root, "core", "domains");
122
+ log(`agents discovered (${agentMap.size})`);
123
+
124
+ // ── Engine setup & brief intake ────────────────────────────
125
+ const engine = new AOSEngine(adapter, config.profileDir, {
126
+ agentsDir: flatAgentsDir,
127
+ domain: config.domainName ?? undefined,
128
+ domainDir: config.domainName ? domainsDir : undefined,
129
+ });
130
+ log("starting engine");
131
+ await engine.start(config.briefPath);
132
+ log("engine started");
133
+
134
+ // ── Bridge server (MCP tool calls → engine) ────────────────
135
+ const sockPath = join(tmpdir(), `aos-bridge-${config.sessionId}.sock`);
136
+ const here = dirname(fileURLToPath(import.meta.url));
137
+ const bridgeScriptPath = join(here, "mcp-arbiter-bridge.ts");
138
+
139
+ let halted = false;
140
+ const steerQueue: string[] = [];
141
+
142
+ async function waitWhileHalted(): Promise<void> {
143
+ while (halted) await new Promise((r) => setTimeout(r, 200));
144
+ }
145
+
146
+ function drainSteer(): string {
147
+ if (steerQueue.length === 0) return "";
148
+ const msgs = steerQueue.splice(0);
149
+ return `\n\n[user steer]\n${msgs.join("\n")}`;
150
+ }
151
+
152
+ log(`starting bridge server sock=${sockPath}`);
153
+ const closeBridge = await startBridgeServer(sockPath, {
154
+ delegate: async (params) => {
155
+ await waitWhileHalted();
156
+ const steer = drainSteer();
157
+ const responses = await engine.delegateMessage(
158
+ params.to as any,
159
+ (params.message as string) + steer,
160
+ );
161
+ const cs = engine.getConstraintState();
162
+ process.stdout.write(
163
+ "\n" +
164
+ renderRoundOneLiner({
165
+ round: cs.rounds_completed,
166
+ maxRounds: 8,
167
+ minutes: cs.elapsed_minutes,
168
+ dollars: cs.budget_spent,
169
+ }) +
170
+ "\n",
171
+ );
172
+ return { responses, constraints: cs };
173
+ },
174
+ end: async (params) => {
175
+ await waitWhileHalted();
176
+ const responses = await engine.end(params.closing_message as string);
177
+ return { ok: true, responses };
178
+ },
179
+ });
180
+
181
+ log("bridge listening");
182
+ // ── Arbiter prompt resolution ──────────────────────────────
183
+ const arbiterDir = agentMap.get("arbiter");
184
+ if (!arbiterDir) throw new Error("No arbiter agent found in core/agents/");
185
+
186
+ const promptPath = join(arbiterDir, "prompt.md");
187
+ const rawPrompt = readFileSync(promptPath, "utf-8");
188
+ const briefContent = readFileSync(config.briefPath, "utf-8");
189
+ const participants = [...agentMap.keys()].filter((id) => id !== "arbiter");
190
+ const memoPath = join(config.deliberationDir, "memo.md");
191
+ const transcriptPath = join(config.deliberationDir, "transcript.jsonl");
192
+ const tools = toolNamesForPlatform(config.platform);
193
+
194
+ const templateVars: Record<string, string> = {
195
+ // Spec-compliant underscore names
196
+ session_id: config.sessionId,
197
+ brief_slug: config.sessionId,
198
+ brief: briefContent,
199
+ format: "brief",
200
+ agent_id: "arbiter",
201
+ agent_name: "Arbiter",
202
+ participants: participants.join(", "),
203
+ constraints: "(see constraint state in tool responses)",
204
+ expertise_block: "",
205
+ output_path: memoPath,
206
+ deliberation_dir: config.deliberationDir,
207
+ transcript_path: transcriptPath,
208
+ delegate_tool: tools.delegate,
209
+ end_tool: tools.end,
210
+ role_override: "",
211
+ // Back-compat hyphenated aliases
212
+ "session-id": config.sessionId,
213
+ "brief-content": briefContent,
214
+ "output-path": memoPath,
215
+ "deliberation-dir": config.deliberationDir,
216
+ "memo-path": memoPath,
217
+ };
218
+
219
+ const resolvedPrompt = resolveTemplate(rawPrompt, templateVars);
220
+
221
+ // The arbiter prompt was authored when tools were named bare (`delegate`, `end`).
222
+ // Claude Code exposes MCP tools as `mcp__aos__delegate` / `mcp__aos__end`, so the
223
+ // model would attempt to call tools that don't exist. Prepend a short preamble
224
+ // that maps the names it'll see to the names the prompt uses.
225
+ const toolPreamble =
226
+ config.platform === "claude-code"
227
+ ? `IMPORTANT — Tool names for this session:\n` +
228
+ `- Where the instructions below say \`delegate(...)\`, call \`${tools.delegate}\` (that is the actual MCP tool name you will see).\n` +
229
+ `- Where they say \`end(...)\`, call \`${tools.end}\`.\n` +
230
+ `- These are the ONLY two tools available to you. Use them exactly as described.\n\n---\n\n`
231
+ : "";
232
+ adapter.setOrchestratorPrompt(toolPreamble + resolvedPrompt);
233
+
234
+ // ── Build MCP args and launch arbiter ──────────────────────
235
+ const mcpOpts = { bridgeScriptPath, socketPath: sockPath };
236
+ const mcpArgs: string[] =
237
+ (agentRuntime as any).buildMcpArgs?.(mcpOpts) ?? [];
238
+
239
+ let restoreGeminiSettings: (() => void) | undefined;
240
+ if ((agentRuntime as any).writeMcpSettings) {
241
+ restoreGeminiSettings = (agentRuntime as any).writeMcpSettings({
242
+ ...mcpOpts,
243
+ projectRoot: config.root,
244
+ });
245
+ }
246
+
247
+ // Load the arbiter AgentConfig from disk (uses the flat dir so
248
+ // loadAgent sees the same shape the engine sees). Override the
249
+ // systemPrompt with the resolved template.
250
+ const arbiterFlatDir = join(flatAgentsDir, "arbiter");
251
+ const arbiterConfig = loadAgent(arbiterFlatDir);
252
+ arbiterConfig.systemPrompt = resolvedPrompt;
253
+
254
+ log("spawning arbiter");
255
+ const arbiterHandle = await adapter.spawnAgent(arbiterConfig, config.sessionId);
256
+
257
+ // ── Kickoff message ────────────────────────────────────────
258
+ const kickoff =
259
+ "Read the brief below and begin the multi-agent deliberation. " +
260
+ `Use the \`${tools.delegate}\` tool to engage perspective agents and ` +
261
+ `\`${tools.end}\` when ready to wrap up.\n\n` +
262
+ `---\n\n## Brief\n\n${briefContent}`;
263
+
264
+ // ── Interactive readline commands ──────────────────────────
265
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
266
+ rl.on("line", (input) => {
267
+ const trimmed = input.trim();
268
+ if (!trimmed) return;
269
+ if (trimmed === "/aos-halt") {
270
+ halted = true;
271
+ console.log("Deliberation paused. Type /aos-resume to continue.");
272
+ } else if (trimmed === "/aos-resume") {
273
+ halted = false;
274
+ console.log("Resumed.");
275
+ } else if (trimmed === "/aos-end") {
276
+ steerQueue.push("Please wrap up now and call the end tool.");
277
+ } else if (trimmed === "/aos-status") {
278
+ const cs = engine.getConstraintState();
279
+ console.log(renderTextGauge("TIME", cs.elapsed_minutes, 2, 10, "min"));
280
+ if (cs.metered) {
281
+ console.log(renderTextGauge("BUDGET", cs.budget_spent, 1, 10, "$"));
282
+ }
283
+ console.log(renderTextGauge("ROUNDS", cs.rounds_completed, 2, 8, "rounds"));
284
+ } else if (trimmed.startsWith("/aos-steer ")) {
285
+ steerQueue.push(trimmed.slice("/aos-steer ".length));
286
+ } else if (!trimmed.startsWith("/")) {
287
+ steerQueue.push(trimmed);
288
+ }
289
+ });
290
+
291
+ try {
292
+ log(`sending kickoff to arbiter (mcpArgs=${mcpArgs.length})`);
293
+ console.log(
294
+ `\nArbiter running (${config.platform}). ` +
295
+ `Tool calls will stream as rounds complete. ` +
296
+ `Bridge socket: ${sockPath}\n`,
297
+ );
298
+ const response = await adapter.sendMessage(arbiterHandle, kickoff, {
299
+ extraArgs: mcpArgs,
300
+ });
301
+ if ((response as any).status !== "success") {
302
+ console.error(
303
+ `\n[arbiter] call failed: status=${(response as any).status} ` +
304
+ `error=${(response as any).error ?? "(none)"}`,
305
+ );
306
+ }
307
+ console.log("\n" + response.text);
308
+ } finally {
309
+ rl.close();
310
+ await closeBridge();
311
+ if (restoreGeminiSettings) restoreGeminiSettings();
312
+ const cs = engine.getConstraintState();
313
+ console.log(
314
+ `\nSession complete. Rounds: ${cs.rounds_completed}, ` +
315
+ `Cost: $${cs.budget_spent.toFixed(4)}, ` +
316
+ `Time: ${cs.elapsed_minutes.toFixed(1)}min`,
317
+ );
318
+ }
319
+ }
@@ -0,0 +1,54 @@
1
+ import { createServer, Socket } from "node:net";
2
+ import { unlinkSync, existsSync } from "node:fs";
3
+
4
+ export interface BridgeHandlers {
5
+ delegate: (params: { to: string | string[]; message: string }) => Promise<unknown>;
6
+ end: (params: { closing_message: string }) => Promise<unknown>;
7
+ }
8
+
9
+ export async function startBridgeServer(
10
+ socketPath: string,
11
+ handlers: BridgeHandlers,
12
+ ): Promise<() => Promise<void>> {
13
+ if (existsSync(socketPath)) unlinkSync(socketPath);
14
+
15
+ const open = new Set<Socket>();
16
+
17
+ const server = createServer((sock: Socket) => {
18
+ open.add(sock);
19
+ sock.on("close", () => open.delete(sock));
20
+ let buf = "";
21
+ sock.on("data", async (chunk) => {
22
+ buf += chunk.toString("utf-8");
23
+ let nl: number;
24
+ while ((nl = buf.indexOf("\n")) >= 0) {
25
+ const line = buf.slice(0, nl);
26
+ buf = buf.slice(nl + 1);
27
+ if (!line.trim()) continue;
28
+ let req: any;
29
+ try { req = JSON.parse(line); } catch { continue; }
30
+ try {
31
+ let result: unknown;
32
+ if (req.method === "delegate") result = await handlers.delegate(req.params);
33
+ else if (req.method === "end") result = await handlers.end(req.params);
34
+ else throw new Error(`unknown method: ${req.method}`);
35
+ sock.write(JSON.stringify({ id: req.id, result }) + "\n");
36
+ } catch (err: any) {
37
+ sock.write(JSON.stringify({ id: req.id, error: String(err?.message ?? err) }) + "\n");
38
+ }
39
+ }
40
+ });
41
+ sock.on("error", () => { /* client disconnect is fine */ });
42
+ });
43
+
44
+ await new Promise<void>((resolve, reject) => {
45
+ server.once("error", reject);
46
+ server.listen(socketPath, () => resolve());
47
+ });
48
+
49
+ return async () => {
50
+ for (const s of open) s.destroy();
51
+ await new Promise<void>((resolve) => server.close(() => resolve()));
52
+ if (existsSync(socketPath)) unlinkSync(socketPath);
53
+ };
54
+ }
@@ -7,6 +7,8 @@ import { join, resolve, basename } from "node:path";
7
7
  import { c, type ParsedArgs } from "../colors";
8
8
  import { getHarnessRoot, discoverDirs, promptSelect, getAdapterDir } from "../utils";
9
9
  import type { TranscriptEntry } from "@aos-harness/runtime/types";
10
+ import { runAdapterSession } from "../adapter-session";
11
+ import { readAdapterConfig } from "../adapter-config";
10
12
 
11
13
  function createEventBuffer(platformUrl: string, sessionId: string) {
12
14
  const buffer: TranscriptEntry[] = [];
@@ -304,7 +306,7 @@ ${c.bold(`AOS ${sessionType} Session`)}
304
306
  Output: ${c.cyan(deliberationDir)}
305
307
  `);
306
308
 
307
- // Check for .aos/config.yaml to determine adapter
309
+ // Determine adapter: --adapter flag > .aos/config.yaml > default "pi"
308
310
  let platformUrl = (args.flags["platform-url"] as string) || null;
309
311
  const aosConfigPath = join(process.cwd(), ".aos", "config.yaml");
310
312
  let adapter = "pi";
@@ -317,6 +319,7 @@ ${c.bold(`AOS ${sessionType} Session`)}
317
319
  platformUrl = (config.platform as Record<string, unknown>).url as string;
318
320
  }
319
321
  }
322
+ if (args.flags["adapter"]) adapter = args.flags["adapter"] as string;
320
323
 
321
324
  const adapterName = adapter === "claude-code" ? "claude-code" : adapter;
322
325
  // Resolve adapter from: 1) project dir, 2) installed package, 3) monorepo
@@ -374,13 +377,19 @@ ${c.bold(`AOS ${sessionType} Session`)}
374
377
  }
375
378
  process.exit(exitCode);
376
379
  } else {
377
- console.log(c.yellow(`Adapter "${adapter}" is not yet fully supported in the CLI.`));
378
- console.log(c.dim(`The framework launched with profile="${profileName}", domain="${domainName || "none"}", brief="${briefPath}".`));
379
- if (isExecutionProfile && workflowConfig) {
380
- console.log(c.dim(`Workflow: ${workflowConfig.id} (${workflowConfig.steps.length} steps)`));
381
- console.log(c.dim(`Workflows dir: ${workflowsDir}`));
382
- }
383
- console.log(c.dim(`Deliberation dir: ${deliberationDir}`));
384
- console.log(c.dim(`Implement the ${adapter} adapter at adapters/${adapter}/ to enable full execution.`));
380
+ const adapterConfig = readAdapterConfig(root);
381
+ await runAdapterSession({
382
+ platform: adapter,
383
+ profileDir: profileDir!,
384
+ briefPath,
385
+ domainName,
386
+ root,
387
+ sessionId,
388
+ deliberationDir,
389
+ verbose: !!args.flags.verbose,
390
+ workflowConfig: isExecutionProfile ? workflowConfig : null,
391
+ workflowsDir,
392
+ modelOverrides: adapterConfig?.model_overrides,
393
+ });
385
394
  }
386
395
  }
package/src/gauges.ts ADDED
@@ -0,0 +1,33 @@
1
+ const BAR_WIDTH = 16;
2
+
3
+ function color(text: string, code: string): string {
4
+ return `\x1b[${code}m${text}\x1b[0m`;
5
+ }
6
+
7
+ export function renderTextGauge(
8
+ label: string,
9
+ value: number,
10
+ min: number,
11
+ max: number,
12
+ unit: string,
13
+ ): string {
14
+ const ratio = Math.max(0, Math.min(1, value / max));
15
+ const filled = Math.round(ratio * BAR_WIDTH);
16
+ const bar = "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
17
+ const colored = value < min ? color(bar, "32") : value >= max * 0.9 ? color(bar, "31") : color(bar, "33");
18
+ const valueStr = unit === "$" ? `$${value.toFixed(2)}` : `${value.toFixed(1)} ${unit}`;
19
+ const minStr = unit === "$" ? `$${min}` : `${min}`;
20
+ const maxStr = unit === "$" ? `$${max}` : `${max}`;
21
+ return ` ${label.padEnd(7)} ${valueStr.padEnd(10)} [${colored}] (min: ${minStr}, max: ${maxStr})`;
22
+ }
23
+
24
+ export interface RoundSummary {
25
+ round: number;
26
+ maxRounds: number;
27
+ minutes: number;
28
+ dollars: number;
29
+ }
30
+
31
+ export function renderRoundOneLiner(s: RoundSummary): string {
32
+ return color(`[Round ${s.round}/${s.maxRounds} · ${s.minutes.toFixed(1)}min · $${s.dollars.toFixed(2)}]`, "90");
33
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env bun
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import { connect, Socket } from "node:net";
9
+ import { randomUUID } from "node:crypto";
10
+
11
+ const SOCK = process.env.AOS_BRIDGE_SOCKET;
12
+ if (!SOCK) {
13
+ console.error("AOS_BRIDGE_SOCKET env var is required");
14
+ process.exit(1);
15
+ }
16
+
17
+ let sock: Socket | null = null;
18
+ const pending = new Map<string, (msg: any) => void>();
19
+ let buf = "";
20
+
21
+ function ensureSock(): Promise<Socket> {
22
+ if (sock && !sock.destroyed) return Promise.resolve(sock);
23
+ return new Promise((resolve, reject) => {
24
+ const s = connect(SOCK!);
25
+ s.on("connect", () => { sock = s; resolve(s); });
26
+ s.on("error", reject);
27
+ s.on("data", (chunk) => {
28
+ buf += chunk.toString("utf-8");
29
+ let nl: number;
30
+ while ((nl = buf.indexOf("\n")) >= 0) {
31
+ const line = buf.slice(0, nl);
32
+ buf = buf.slice(nl + 1);
33
+ if (!line.trim()) continue;
34
+ try {
35
+ const msg = JSON.parse(line);
36
+ const cb = pending.get(msg.id);
37
+ if (cb) { pending.delete(msg.id); cb(msg); }
38
+ } catch { /* ignore */ }
39
+ }
40
+ });
41
+ s.on("close", () => { sock = null; });
42
+ });
43
+ }
44
+
45
+ async function rpc(method: string, params: unknown): Promise<unknown> {
46
+ const s = await ensureSock();
47
+ const id = randomUUID();
48
+ return new Promise((resolve, reject) => {
49
+ pending.set(id, (msg) => {
50
+ if (msg.error) reject(new Error(msg.error));
51
+ else resolve(msg.result);
52
+ });
53
+ s.write(JSON.stringify({ id, method, params }) + "\n");
54
+ });
55
+ }
56
+
57
+ const server = new Server(
58
+ { name: "aos-arbiter-bridge", version: "0.1.0" },
59
+ { capabilities: { tools: {} } },
60
+ );
61
+
62
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
63
+ tools: [
64
+ {
65
+ name: "delegate",
66
+ description: "Delegate a message to one or more participant agents and receive their responses.",
67
+ inputSchema: {
68
+ type: "object" as const,
69
+ properties: {
70
+ to: { description: "Agent id, list of ids, or 'all'", type: "string" },
71
+ message: { type: "string" },
72
+ },
73
+ required: ["to", "message"],
74
+ },
75
+ },
76
+ {
77
+ name: "end",
78
+ description: "End the deliberation. Provide a closing summary message.",
79
+ inputSchema: {
80
+ type: "object" as const,
81
+ properties: { closing_message: { type: "string" } },
82
+ required: ["closing_message"],
83
+ },
84
+ },
85
+ ],
86
+ }));
87
+
88
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
89
+ const result = await rpc(req.params.name, req.params.arguments ?? {});
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
92
+ };
93
+ });
94
+
95
+ await server.connect(new StdioServerTransport());