@zhijiewang/openharness 2.0.0 → 2.3.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 (235) hide show
  1. package/README.md +4 -4
  2. package/dist/DeferredTool.js +3 -1
  3. package/dist/Tool.d.ts +1 -1
  4. package/dist/agents/roles.js +58 -62
  5. package/dist/commands/cybergotchi.d.ts +1 -1
  6. package/dist/commands/cybergotchi.js +30 -30
  7. package/dist/commands/index.js +360 -122
  8. package/dist/components/App.d.ts +1 -1
  9. package/dist/components/App.js +6 -6
  10. package/dist/components/CompanionFooter.d.ts +1 -1
  11. package/dist/components/CompanionFooter.js +6 -8
  12. package/dist/components/CybergotchiBubble.js +5 -5
  13. package/dist/components/CybergotchiPanel.d.ts +1 -1
  14. package/dist/components/CybergotchiPanel.js +7 -7
  15. package/dist/components/CybergotchiPanelConnected.js +2 -2
  16. package/dist/components/CybergotchiSetup.js +26 -24
  17. package/dist/components/CybergotchiSprite.d.ts +1 -1
  18. package/dist/components/CybergotchiSprite.js +8 -12
  19. package/dist/components/DiffView.d.ts +1 -1
  20. package/dist/components/DiffView.js +10 -10
  21. package/dist/components/ErrorBoundary.d.ts +1 -1
  22. package/dist/components/ErrorBoundary.js +1 -1
  23. package/dist/components/InitWizard.js +65 -33
  24. package/dist/components/Markdown.js +2 -4
  25. package/dist/components/Messages.js +4 -4
  26. package/dist/components/PermissionPrompt.d.ts +1 -1
  27. package/dist/components/PermissionPrompt.js +15 -17
  28. package/dist/components/REPL.d.ts +1 -1
  29. package/dist/components/REPL.js +74 -49
  30. package/dist/components/Spinner.js +2 -2
  31. package/dist/components/TextInput.js +35 -29
  32. package/dist/components/ToolCallDisplay.js +3 -5
  33. package/dist/cybergotchi/bones.d.ts +1 -1
  34. package/dist/cybergotchi/bones.js +8 -8
  35. package/dist/cybergotchi/config.d.ts +2 -2
  36. package/dist/cybergotchi/config.js +13 -13
  37. package/dist/cybergotchi/events.d.ts +5 -5
  38. package/dist/cybergotchi/events.js +7 -7
  39. package/dist/cybergotchi/needs.d.ts +2 -2
  40. package/dist/cybergotchi/needs.js +7 -9
  41. package/dist/cybergotchi/personality.d.ts +2 -2
  42. package/dist/cybergotchi/personality.js +2 -2
  43. package/dist/cybergotchi/species.d.ts +1 -1
  44. package/dist/cybergotchi/species.js +145 -217
  45. package/dist/cybergotchi/speech.d.ts +2 -2
  46. package/dist/cybergotchi/speech.js +43 -43
  47. package/dist/cybergotchi/types.d.ts +4 -4
  48. package/dist/cybergotchi/types.js +26 -26
  49. package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
  50. package/dist/cybergotchi/useCybergotchi.js +29 -25
  51. package/dist/git/index.js +11 -9
  52. package/dist/harness/checkpoints.js +29 -21
  53. package/dist/harness/config.d.ts +12 -2
  54. package/dist/harness/config.js +15 -9
  55. package/dist/harness/context-warning.d.ts +1 -1
  56. package/dist/harness/context-warning.js +1 -1
  57. package/dist/harness/cost.js +1 -1
  58. package/dist/harness/credentials.js +13 -13
  59. package/dist/harness/hooks.js +7 -5
  60. package/dist/harness/keybindings.js +20 -18
  61. package/dist/harness/marketplace.d.ts +3 -3
  62. package/dist/harness/marketplace.js +55 -42
  63. package/dist/harness/memory.d.ts +23 -5
  64. package/dist/harness/memory.js +142 -41
  65. package/dist/harness/onboarding.js +30 -10
  66. package/dist/harness/plugins.d.ts +9 -1
  67. package/dist/harness/plugins.js +54 -30
  68. package/dist/harness/rules.js +12 -7
  69. package/dist/harness/sandbox.d.ts +34 -0
  70. package/dist/harness/sandbox.js +104 -0
  71. package/dist/harness/session-db.d.ts +55 -0
  72. package/dist/harness/session-db.js +165 -0
  73. package/dist/harness/session.d.ts +1 -1
  74. package/dist/harness/session.js +34 -15
  75. package/dist/harness/store.d.ts +3 -3
  76. package/dist/harness/store.js +6 -4
  77. package/dist/harness/submit-handler.d.ts +4 -4
  78. package/dist/harness/submit-handler.js +57 -21
  79. package/dist/harness/telemetry.d.ts +1 -1
  80. package/dist/harness/telemetry.js +23 -19
  81. package/dist/harness/traces.d.ts +2 -2
  82. package/dist/harness/traces.js +44 -33
  83. package/dist/harness/verification.d.ts +1 -1
  84. package/dist/harness/verification.js +50 -44
  85. package/dist/lsp/client.js +44 -40
  86. package/dist/main.js +100 -59
  87. package/dist/mcp/DeferredMcpTool.d.ts +4 -4
  88. package/dist/mcp/DeferredMcpTool.js +9 -5
  89. package/dist/mcp/McpTool.d.ts +4 -4
  90. package/dist/mcp/McpTool.js +8 -4
  91. package/dist/mcp/client.d.ts +2 -2
  92. package/dist/mcp/client.js +21 -21
  93. package/dist/mcp/loader.d.ts +1 -1
  94. package/dist/mcp/loader.js +17 -12
  95. package/dist/mcp/registry.d.ts +3 -3
  96. package/dist/mcp/registry.js +97 -97
  97. package/dist/mcp/schema.d.ts +1 -1
  98. package/dist/mcp/schema.js +16 -16
  99. package/dist/mcp/server.d.ts +1 -1
  100. package/dist/mcp/server.js +21 -21
  101. package/dist/mcp/types.d.ts +3 -3
  102. package/dist/providers/anthropic.d.ts +2 -2
  103. package/dist/providers/anthropic.js +10 -9
  104. package/dist/providers/base.d.ts +1 -1
  105. package/dist/providers/index.js +10 -3
  106. package/dist/providers/llamacpp.d.ts +2 -2
  107. package/dist/providers/llamacpp.js +1 -3
  108. package/dist/providers/ollama.d.ts +2 -2
  109. package/dist/providers/ollama.js +3 -4
  110. package/dist/providers/openai.d.ts +2 -2
  111. package/dist/providers/openai.js +3 -5
  112. package/dist/providers/openrouter.d.ts +2 -2
  113. package/dist/providers/router.d.ts +1 -1
  114. package/dist/providers/router.js +7 -7
  115. package/dist/query/compress.d.ts +2 -2
  116. package/dist/query/compress.js +22 -21
  117. package/dist/query/context-manager.d.ts +2 -2
  118. package/dist/query/context-manager.js +8 -11
  119. package/dist/query/errors.js +1 -1
  120. package/dist/query/index.d.ts +1 -1
  121. package/dist/query/index.js +30 -22
  122. package/dist/query/tools.js +15 -12
  123. package/dist/query/types.d.ts +1 -1
  124. package/dist/query.d.ts +1 -1
  125. package/dist/query.js +1 -1
  126. package/dist/remote/auth.d.ts +2 -2
  127. package/dist/remote/auth.js +8 -8
  128. package/dist/remote/server.d.ts +3 -3
  129. package/dist/remote/server.js +60 -60
  130. package/dist/renderer/cells.js +9 -9
  131. package/dist/renderer/colors.js +24 -6
  132. package/dist/renderer/diff.d.ts +2 -2
  133. package/dist/renderer/diff.js +27 -19
  134. package/dist/renderer/differ.d.ts +1 -1
  135. package/dist/renderer/differ.js +9 -9
  136. package/dist/renderer/image.js +19 -19
  137. package/dist/renderer/index.d.ts +6 -6
  138. package/dist/renderer/index.js +163 -93
  139. package/dist/renderer/input.js +66 -48
  140. package/dist/renderer/layout.d.ts +6 -6
  141. package/dist/renderer/layout.js +163 -124
  142. package/dist/renderer/markdown.d.ts +2 -2
  143. package/dist/renderer/markdown.js +173 -54
  144. package/dist/renderer/session-browser.d.ts +2 -2
  145. package/dist/renderer/session-browser.js +19 -21
  146. package/dist/repl.d.ts +5 -5
  147. package/dist/repl.js +300 -198
  148. package/dist/sdk/index.d.ts +8 -7
  149. package/dist/sdk/index.js +59 -42
  150. package/dist/services/AgentDispatcher.d.ts +3 -3
  151. package/dist/services/AgentDispatcher.js +33 -29
  152. package/dist/services/CronExecutor.d.ts +4 -4
  153. package/dist/services/CronExecutor.js +12 -8
  154. package/dist/services/EvaluatorLoop.d.ts +3 -3
  155. package/dist/services/EvaluatorLoop.js +29 -21
  156. package/dist/services/MetaHarness.d.ts +1 -1
  157. package/dist/services/MetaHarness.js +41 -33
  158. package/dist/services/PipelineExecutor.d.ts +1 -1
  159. package/dist/services/PipelineExecutor.js +23 -25
  160. package/dist/services/SkillExtractor.d.ts +43 -0
  161. package/dist/services/SkillExtractor.js +143 -0
  162. package/dist/services/StreamingToolExecutor.d.ts +2 -2
  163. package/dist/services/StreamingToolExecutor.js +11 -7
  164. package/dist/services/a2a.d.ts +8 -8
  165. package/dist/services/a2a.js +44 -34
  166. package/dist/services/agent-messaging.d.ts +33 -15
  167. package/dist/services/agent-messaging.js +65 -13
  168. package/dist/services/cron.js +16 -16
  169. package/dist/tools/AgentTool/index.d.ts +5 -2
  170. package/dist/tools/AgentTool/index.js +35 -15
  171. package/dist/tools/AskUserTool/index.js +1 -1
  172. package/dist/tools/BashTool/index.d.ts +2 -2
  173. package/dist/tools/BashTool/index.js +18 -10
  174. package/dist/tools/CronTool/index.d.ts +2 -2
  175. package/dist/tools/CronTool/index.js +30 -12
  176. package/dist/tools/DiagnosticsTool/index.js +28 -22
  177. package/dist/tools/EnterPlanModeTool/index.js +93 -14
  178. package/dist/tools/EnterWorktreeTool/index.js +7 -3
  179. package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
  180. package/dist/tools/ExitPlanModeTool/index.js +20 -5
  181. package/dist/tools/ExitWorktreeTool/index.js +11 -4
  182. package/dist/tools/FileEditTool/index.js +3 -5
  183. package/dist/tools/FileReadTool/index.js +16 -10
  184. package/dist/tools/FileWriteTool/index.js +2 -2
  185. package/dist/tools/GlobTool/index.js +5 -9
  186. package/dist/tools/GrepTool/index.d.ts +2 -2
  187. package/dist/tools/GrepTool/index.js +14 -9
  188. package/dist/tools/ImageReadTool/index.js +2 -2
  189. package/dist/tools/KillProcessTool/index.js +11 -7
  190. package/dist/tools/LSTool/index.js +3 -3
  191. package/dist/tools/MemoryTool/index.d.ts +11 -11
  192. package/dist/tools/MemoryTool/index.js +28 -14
  193. package/dist/tools/MonitorTool/index.d.ts +2 -2
  194. package/dist/tools/MonitorTool/index.js +24 -19
  195. package/dist/tools/MultiEditTool/index.js +9 -5
  196. package/dist/tools/NotebookEditTool/index.js +3 -3
  197. package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
  198. package/dist/tools/ParallelAgentTool/index.js +12 -6
  199. package/dist/tools/PipelineTool/index.d.ts +4 -4
  200. package/dist/tools/PipelineTool/index.js +3 -3
  201. package/dist/tools/PowerShellTool/index.js +10 -6
  202. package/dist/tools/RemoteTriggerTool/index.js +8 -4
  203. package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
  204. package/dist/tools/ScheduleWakeupTool/index.js +115 -0
  205. package/dist/tools/SendMessageTool/index.js +25 -7
  206. package/dist/tools/SessionSearchTool/index.d.ts +15 -0
  207. package/dist/tools/SessionSearchTool/index.js +36 -0
  208. package/dist/tools/SkillTool/index.d.ts +3 -0
  209. package/dist/tools/SkillTool/index.js +39 -9
  210. package/dist/tools/TaskCreateTool/index.d.ts +2 -2
  211. package/dist/tools/TaskCreateTool/index.js +2 -2
  212. package/dist/tools/TaskGetTool/index.js +2 -2
  213. package/dist/tools/TaskListTool/index.js +3 -5
  214. package/dist/tools/TaskOutputTool/index.js +2 -2
  215. package/dist/tools/TaskStopTool/index.js +3 -3
  216. package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
  217. package/dist/tools/TaskUpdateTool/index.js +2 -2
  218. package/dist/tools/ToolSearchTool/index.js +9 -6
  219. package/dist/tools/WebFetchTool/index.js +1 -1
  220. package/dist/tools/WebSearchTool/index.js +2 -6
  221. package/dist/tools.js +31 -30
  222. package/dist/types/permissions.js +15 -9
  223. package/dist/utils/bash-safety.d.ts +1 -1
  224. package/dist/utils/bash-safety.js +64 -54
  225. package/dist/utils/diff-algorithm.d.ts +3 -3
  226. package/dist/utils/diff-algorithm.js +7 -7
  227. package/dist/utils/fs.js +3 -3
  228. package/dist/utils/safe-env.js +1 -1
  229. package/dist/utils/theme-data.d.ts +1 -1
  230. package/dist/utils/theme-data.js +1 -1
  231. package/dist/utils/theme.d.ts +1 -1
  232. package/dist/utils/theme.js +1 -1
  233. package/dist/utils/tool-summary.d.ts +1 -1
  234. package/dist/utils/tool-summary.js +27 -9
  235. package/package.json +10 -3
@@ -12,10 +12,10 @@
12
12
  *
13
13
  * Based on the emerging A2A (Agent-to-Agent) protocol standard.
14
14
  */
15
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, unlinkSync } from 'node:fs';
16
- import { join } from 'node:path';
17
- import { homedir } from 'node:os';
18
- const AGENT_REGISTRY_DIR = join(homedir(), '.oh', 'agents');
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+ const AGENT_REGISTRY_DIR = join(homedir(), ".oh", "agents");
19
19
  // ── Registry Operations ──
20
20
  /** Publish an agent card to the shared registry */
21
21
  export function publishCard(card) {
@@ -29,16 +29,18 @@ export function unpublishCard(agentId) {
29
29
  try {
30
30
  unlinkSync(filePath);
31
31
  }
32
- catch { /* ignore */ }
32
+ catch {
33
+ /* ignore */
34
+ }
33
35
  }
34
36
  /** Discover all registered agents */
35
37
  export function discoverAgents() {
36
38
  if (!existsSync(AGENT_REGISTRY_DIR))
37
39
  return [];
38
40
  const cards = [];
39
- for (const file of readdirSync(AGENT_REGISTRY_DIR).filter(f => f.endsWith('.json'))) {
41
+ for (const file of readdirSync(AGENT_REGISTRY_DIR).filter((f) => f.endsWith(".json"))) {
40
42
  try {
41
- const raw = readFileSync(join(AGENT_REGISTRY_DIR, file), 'utf-8');
43
+ const raw = readFileSync(join(AGENT_REGISTRY_DIR, file), "utf-8");
42
44
  const card = JSON.parse(raw);
43
45
  // Check if the agent process is still alive
44
46
  if (isProcessAlive(card.pid)) {
@@ -49,20 +51,24 @@ export function discoverAgents() {
49
51
  try {
50
52
  unlinkSync(join(AGENT_REGISTRY_DIR, file));
51
53
  }
52
- catch { /* ignore */ }
54
+ catch {
55
+ /* ignore */
56
+ }
53
57
  }
54
58
  }
55
- catch { /* skip malformed cards */ }
59
+ catch {
60
+ /* skip malformed cards */
61
+ }
56
62
  }
57
63
  return cards;
58
64
  }
59
65
  /** Find agents by capability name */
60
66
  export function findAgentsByCapability(capabilityName) {
61
- return discoverAgents().filter(card => card.capabilities.some(c => c.name.toLowerCase() === capabilityName.toLowerCase()));
67
+ return discoverAgents().filter((card) => card.capabilities.some((c) => c.name.toLowerCase() === capabilityName.toLowerCase()));
62
68
  }
63
69
  /** Find an agent by name */
64
70
  export function findAgentByName(name) {
65
- return discoverAgents().find(c => c.name.toLowerCase() === name.toLowerCase()) ?? null;
71
+ return discoverAgents().find((c) => c.name.toLowerCase() === name.toLowerCase()) ?? null;
66
72
  }
67
73
  // ── Message Routing ──
68
74
  /**
@@ -75,13 +81,13 @@ export async function routeMessage(message) {
75
81
  let targetCard = null;
76
82
  // Try by agent ID first
77
83
  const agents = discoverAgents();
78
- targetCard = agents.find(a => a.id === message.to) ?? null;
84
+ targetCard = agents.find((a) => a.id === message.to) ?? null;
79
85
  // Try by name
80
86
  if (!targetCard) {
81
- targetCard = agents.find(a => a.name.toLowerCase() === message.to.toLowerCase()) ?? null;
87
+ targetCard = agents.find((a) => a.name.toLowerCase() === message.to.toLowerCase()) ?? null;
82
88
  }
83
89
  // Try by capability
84
- if (!targetCard && message.type === 'task' && message.payload.kind === 'task') {
90
+ if (!targetCard && message.type === "task" && message.payload.kind === "task") {
85
91
  const capable = findAgentsByCapability(message.payload.capability);
86
92
  if (capable.length > 0)
87
93
  targetCard = capable[0];
@@ -90,25 +96,27 @@ export async function routeMessage(message) {
90
96
  return null;
91
97
  // Route based on endpoint type
92
98
  switch (targetCard.endpoint.type) {
93
- case 'http': {
99
+ case "http": {
94
100
  try {
95
- const url = `${targetCard.endpoint.address}${targetCard.endpoint.port ? ':' + targetCard.endpoint.port : ''}/a2a`;
101
+ const url = `${targetCard.endpoint.address}${targetCard.endpoint.port ? `:${targetCard.endpoint.port}` : ""}/a2a`;
96
102
  const res = await fetch(url, {
97
- method: 'POST',
98
- headers: { 'Content-Type': 'application/json' },
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
99
105
  body: JSON.stringify(message),
100
106
  signal: AbortSignal.timeout(30_000),
101
107
  });
102
108
  if (res.ok) {
103
- return await res.json();
109
+ return (await res.json());
104
110
  }
105
111
  }
106
- catch { /* delivery failed */ }
112
+ catch {
113
+ /* delivery failed */
114
+ }
107
115
  return null;
108
116
  }
109
- case 'ipc': {
117
+ case "ipc": {
110
118
  // File-based inbox for local IPC
111
- const inboxDir = join(AGENT_REGISTRY_DIR, 'inboxes', targetCard.id);
119
+ const inboxDir = join(AGENT_REGISTRY_DIR, "inboxes", targetCard.id);
112
120
  mkdirSync(inboxDir, { recursive: true });
113
121
  const msgFile = join(inboxDir, `${message.id}.json`);
114
122
  writeFileSync(msgFile, JSON.stringify(message, null, 2));
@@ -120,18 +128,20 @@ export async function routeMessage(message) {
120
128
  }
121
129
  /** Read pending messages from an agent's inbox */
122
130
  export function readInbox(agentId) {
123
- const inboxDir = join(AGENT_REGISTRY_DIR, 'inboxes', agentId);
131
+ const inboxDir = join(AGENT_REGISTRY_DIR, "inboxes", agentId);
124
132
  if (!existsSync(inboxDir))
125
133
  return [];
126
134
  const messages = [];
127
- for (const file of readdirSync(inboxDir).filter(f => f.endsWith('.json'))) {
135
+ for (const file of readdirSync(inboxDir).filter((f) => f.endsWith(".json"))) {
128
136
  try {
129
- const raw = readFileSync(join(inboxDir, file), 'utf-8');
137
+ const raw = readFileSync(join(inboxDir, file), "utf-8");
130
138
  messages.push(JSON.parse(raw));
131
139
  // Remove after reading
132
140
  unlinkSync(join(inboxDir, file));
133
141
  }
134
- catch { /* skip */ }
142
+ catch {
143
+ /* skip */
144
+ }
135
145
  }
136
146
  return messages.sort((a, b) => a.timestamp - b.timestamp);
137
147
  }
@@ -155,17 +165,17 @@ export function createSessionCard(sessionId, opts = {}) {
155
165
  return {
156
166
  id: `oh-${sessionId}`,
157
167
  name: `openharness-${sessionId.slice(0, 6)}`,
158
- version: '1.0.0',
168
+ version: "1.0.0",
159
169
  capabilities: [
160
- { name: 'code-generation', description: 'Generate, edit, and review code' },
161
- { name: 'code-review', description: 'Review code for bugs and quality' },
162
- { name: 'test-generation', description: 'Write tests for existing code' },
163
- { name: 'file-operations', description: 'Read, write, search files' },
164
- { name: 'bash-execution', description: 'Run shell commands' },
170
+ { name: "code-generation", description: "Generate, edit, and review code" },
171
+ { name: "code-review", description: "Review code for bugs and quality" },
172
+ { name: "test-generation", description: "Write tests for existing code" },
173
+ { name: "file-operations", description: "Read, write, search files" },
174
+ { name: "bash-execution", description: "Run shell commands" },
165
175
  ],
166
176
  endpoint: opts.port
167
- ? { type: 'http', address: 'http://localhost', port: opts.port }
168
- : { type: 'ipc', address: join(AGENT_REGISTRY_DIR, 'inboxes', `oh-${sessionId}`) },
177
+ ? { type: "http", address: "http://localhost", port: opts.port }
178
+ : { type: "ipc", address: join(AGENT_REGISTRY_DIR, "inboxes", `oh-${sessionId}`) },
169
179
  registeredAt: Date.now(),
170
180
  pid: process.pid,
171
181
  provider: opts.provider,
@@ -1,18 +1,7 @@
1
- /**
2
- * Agent messaging — peer-to-peer communication between sub-agents.
3
- *
4
- * Provides a message bus for agents to send/receive messages, share state,
5
- * and coordinate work. Uses file-based locking for concurrent file access.
6
- *
7
- * Architecture (inspired by Claude Code Agent Teams):
8
- * - Each agent has a unique ID
9
- * - Messages are stored in a shared inbox (in-memory for same-process agents)
10
- * - File locking prevents concurrent edits to the same file
11
- */
12
1
  export type AgentMessage = {
13
2
  from: string;
14
3
  to: string;
15
- type: 'request' | 'response' | 'status' | 'error';
4
+ type: "request" | "response" | "status" | "error";
16
5
  content: string;
17
6
  timestamp: number;
18
7
  metadata?: Record<string, unknown>;
@@ -20,9 +9,23 @@ export type AgentMessage = {
20
9
  export type AgentInfo = {
21
10
  id: string;
22
11
  role: string;
23
- status: 'idle' | 'working' | 'done' | 'error';
12
+ status: "idle" | "working" | "done" | "error";
24
13
  currentTask?: string;
25
14
  };
15
+ /**
16
+ * Background agent entry — tracks running/completed background agents
17
+ * so they can be continued via SendMessage.
18
+ */
19
+ export type BackgroundAgent = {
20
+ id: string;
21
+ role: string;
22
+ status: "running" | "completed" | "error";
23
+ startedAt: number;
24
+ completedAt?: number;
25
+ result?: string;
26
+ /** Queue of messages sent to this agent while it was running */
27
+ pendingMessages: string[];
28
+ };
26
29
  /**
27
30
  * Message bus for agent-to-agent communication.
28
31
  */
@@ -30,22 +33,37 @@ export declare class AgentMessageBus {
30
33
  private inboxes;
31
34
  private agents;
32
35
  private fileLocks;
36
+ private backgroundAgents;
33
37
  /** Register an agent with the bus */
34
38
  registerAgent(id: string, role: string): void;
35
39
  /** Unregister an agent */
36
40
  unregisterAgent(id: string): void;
37
41
  /** Send a message to a specific agent or broadcast */
38
- send(message: Omit<AgentMessage, 'timestamp'>): void;
42
+ send(message: Omit<AgentMessage, "timestamp">): void;
39
43
  /** Receive pending messages for an agent (drains inbox) */
40
44
  receive(agentId: string): AgentMessage[];
41
45
  /** Peek at inbox without draining */
42
46
  peek(agentId: string): AgentMessage[];
43
47
  /** Update agent status */
44
- updateStatus(agentId: string, status: AgentInfo['status'], currentTask?: string): void;
48
+ updateStatus(agentId: string, status: AgentInfo["status"], currentTask?: string): void;
45
49
  /** Get all registered agents */
46
50
  getAgents(): AgentInfo[];
47
51
  /** Get a specific agent's info */
48
52
  getAgent(id: string): AgentInfo | undefined;
53
+ /** Register a background agent for later continuation */
54
+ registerBackgroundAgent(id: string, role: string): void;
55
+ /** Mark a background agent as completed with its result */
56
+ completeBackgroundAgent(id: string, result: string): void;
57
+ /** Mark a background agent as errored */
58
+ errorBackgroundAgent(id: string, error: string): void;
59
+ /** Queue a message for a background agent */
60
+ sendToBackgroundAgent(id: string, content: string): boolean;
61
+ /** Get a background agent's info */
62
+ getBackgroundAgent(id: string): BackgroundAgent | undefined;
63
+ /** List all background agents */
64
+ getBackgroundAgents(): BackgroundAgent[];
65
+ /** Drain pending messages for a background agent */
66
+ drainBackgroundMessages(id: string): string[];
49
67
  /** Acquire a lock on a file path. Returns true if acquired, false if already locked. */
50
68
  acquireLock(agentId: string, filePath: string): boolean;
51
69
  /** Release a lock on a file path */
@@ -1,14 +1,3 @@
1
- /**
2
- * Agent messaging — peer-to-peer communication between sub-agents.
3
- *
4
- * Provides a message bus for agents to send/receive messages, share state,
5
- * and coordinate work. Uses file-based locking for concurrent file access.
6
- *
7
- * Architecture (inspired by Claude Code Agent Teams):
8
- * - Each agent has a unique ID
9
- * - Messages are stored in a shared inbox (in-memory for same-process agents)
10
- * - File locking prevents concurrent edits to the same file
11
- */
12
1
  /**
13
2
  * Message bus for agent-to-agent communication.
14
3
  */
@@ -16,9 +5,10 @@ export class AgentMessageBus {
16
5
  inboxes = new Map();
17
6
  agents = new Map();
18
7
  fileLocks = new Map(); // filePath → agentId
8
+ backgroundAgents = new Map();
19
9
  /** Register an agent with the bus */
20
10
  registerAgent(id, role) {
21
- this.agents.set(id, { id, role, status: 'idle' });
11
+ this.agents.set(id, { id, role, status: "idle" });
22
12
  this.inboxes.set(id, []);
23
13
  }
24
14
  /** Unregister an agent */
@@ -34,7 +24,7 @@ export class AgentMessageBus {
34
24
  /** Send a message to a specific agent or broadcast */
35
25
  send(message) {
36
26
  const msg = { ...message, timestamp: Date.now() };
37
- if (msg.to === '*') {
27
+ if (msg.to === "*") {
38
28
  // Broadcast to all agents except sender
39
29
  for (const [id, inbox] of this.inboxes) {
40
30
  if (id !== msg.from)
@@ -76,6 +66,68 @@ export class AgentMessageBus {
76
66
  getAgent(id) {
77
67
  return this.agents.get(id);
78
68
  }
69
+ // ── Background Agent Registry ──
70
+ /** Register a background agent for later continuation */
71
+ registerBackgroundAgent(id, role) {
72
+ this.backgroundAgents.set(id, {
73
+ id,
74
+ role,
75
+ status: "running",
76
+ startedAt: Date.now(),
77
+ pendingMessages: [],
78
+ });
79
+ // Evict completed/errored agents older than 30 minutes to prevent unbounded growth
80
+ const EVICT_AGE_MS = 30 * 60 * 1000;
81
+ const now = Date.now();
82
+ for (const [agentId, agent] of this.backgroundAgents) {
83
+ if (agent.status !== "running" && agent.completedAt && now - agent.completedAt > EVICT_AGE_MS) {
84
+ this.backgroundAgents.delete(agentId);
85
+ }
86
+ }
87
+ }
88
+ /** Mark a background agent as completed with its result */
89
+ completeBackgroundAgent(id, result) {
90
+ const agent = this.backgroundAgents.get(id);
91
+ if (agent) {
92
+ agent.status = "completed";
93
+ agent.completedAt = Date.now();
94
+ agent.result = result;
95
+ }
96
+ }
97
+ /** Mark a background agent as errored */
98
+ errorBackgroundAgent(id, error) {
99
+ const agent = this.backgroundAgents.get(id);
100
+ if (agent) {
101
+ agent.status = "error";
102
+ agent.completedAt = Date.now();
103
+ agent.result = error;
104
+ }
105
+ }
106
+ /** Queue a message for a background agent */
107
+ sendToBackgroundAgent(id, content) {
108
+ const agent = this.backgroundAgents.get(id);
109
+ if (!agent)
110
+ return false;
111
+ agent.pendingMessages.push(content);
112
+ return true;
113
+ }
114
+ /** Get a background agent's info */
115
+ getBackgroundAgent(id) {
116
+ return this.backgroundAgents.get(id);
117
+ }
118
+ /** List all background agents */
119
+ getBackgroundAgents() {
120
+ return [...this.backgroundAgents.values()];
121
+ }
122
+ /** Drain pending messages for a background agent */
123
+ drainBackgroundMessages(id) {
124
+ const agent = this.backgroundAgents.get(id);
125
+ if (!agent || agent.pendingMessages.length === 0)
126
+ return [];
127
+ const msgs = [...agent.pendingMessages];
128
+ agent.pendingMessages.length = 0;
129
+ return msgs;
130
+ }
79
131
  // ── File Locking ──
80
132
  /** Acquire a lock on a file path. Returns true if acquired, false if already locked. */
81
133
  acquireLock(agentId, filePath) {
@@ -7,19 +7,19 @@
7
7
  * This is a simple implementation using setInterval for minute-level granularity.
8
8
  * For production use, consider node-cron or similar.
9
9
  */
10
- import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, unlinkSync } from 'node:fs';
11
- import { join } from 'node:path';
12
- import { homedir } from 'node:os';
13
- const CRON_DIR = join(homedir(), '.oh', 'crons');
10
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ const CRON_DIR = join(homedir(), ".oh", "crons");
14
14
  /** List all cron definitions */
15
15
  export function listCrons() {
16
16
  if (!existsSync(CRON_DIR))
17
17
  return [];
18
18
  return readdirSync(CRON_DIR)
19
- .filter(f => f.endsWith('.json'))
20
- .map(f => {
19
+ .filter((f) => f.endsWith(".json"))
20
+ .map((f) => {
21
21
  try {
22
- return JSON.parse(readFileSync(join(CRON_DIR, f), 'utf-8'));
22
+ return JSON.parse(readFileSync(join(CRON_DIR, f), "utf-8"));
23
23
  }
24
24
  catch {
25
25
  return null;
@@ -61,19 +61,19 @@ export function parseScheduleMs(schedule) {
61
61
  // "every 5m" → 5 minutes
62
62
  const minMatch = schedule.match(/^every\s+(\d+)\s*m(?:in(?:ute)?s?)?$/i);
63
63
  if (minMatch)
64
- return parseInt(minMatch[1]) * 60 * 1000;
64
+ return parseInt(minMatch[1], 10) * 60 * 1000;
65
65
  // "every 2h" → 2 hours
66
66
  const hourMatch = schedule.match(/^every\s+(\d+)\s*h(?:ours?)?$/i);
67
67
  if (hourMatch)
68
- return parseInt(hourMatch[1]) * 60 * 60 * 1000;
68
+ return parseInt(hourMatch[1], 10) * 60 * 60 * 1000;
69
69
  // "every 1d" → 1 day
70
70
  const dayMatch = schedule.match(/^every\s+(\d+)\s*d(?:ays?)?$/i);
71
71
  if (dayMatch)
72
- return parseInt(dayMatch[1]) * 24 * 60 * 60 * 1000;
72
+ return parseInt(dayMatch[1], 10) * 24 * 60 * 60 * 1000;
73
73
  return null;
74
74
  }
75
75
  // ── Result Persistence ──
76
- const HISTORY_DIR = join(CRON_DIR, 'history');
76
+ const HISTORY_DIR = join(CRON_DIR, "history");
77
77
  /** Save a cron execution result to history */
78
78
  export function saveCronResult(result) {
79
79
  mkdirSync(HISTORY_DIR, { recursive: true });
@@ -85,13 +85,13 @@ export function getCronHistory(cronId, limit = 10) {
85
85
  if (!existsSync(HISTORY_DIR))
86
86
  return [];
87
87
  return readdirSync(HISTORY_DIR)
88
- .filter(f => f.startsWith(`${cronId}-`) && f.endsWith('.json'))
88
+ .filter((f) => f.startsWith(`${cronId}-`) && f.endsWith(".json"))
89
89
  .sort()
90
90
  .reverse()
91
91
  .slice(0, limit)
92
- .map(f => {
92
+ .map((f) => {
93
93
  try {
94
- return JSON.parse(readFileSync(join(HISTORY_DIR, f), 'utf-8'));
94
+ return JSON.parse(readFileSync(join(HISTORY_DIR, f), "utf-8"));
95
95
  }
96
96
  catch {
97
97
  return null;
@@ -104,14 +104,14 @@ export function getCronHistory(cronId, limit = 10) {
104
104
  */
105
105
  export function getDueCrons(crons) {
106
106
  const now = Date.now();
107
- return crons.filter(c => {
107
+ return crons.filter((c) => {
108
108
  if (!c.enabled)
109
109
  return false;
110
110
  const intervalMs = parseScheduleMs(c.schedule);
111
111
  if (!intervalMs)
112
112
  return false;
113
113
  const lastRun = c.lastRun ?? c.createdAt;
114
- return (now - lastRun) >= intervalMs;
114
+ return now - lastRun >= intervalMs;
115
115
  });
116
116
  }
117
117
  //# sourceMappingURL=cron.js.map
@@ -4,6 +4,7 @@ declare const inputSchema: z.ZodObject<{
4
4
  prompt: z.ZodString;
5
5
  description: z.ZodOptional<z.ZodString>;
6
6
  isolated: z.ZodOptional<z.ZodBoolean>;
7
+ isolation: z.ZodOptional<z.ZodEnum<["worktree"]>>;
7
8
  run_in_background: z.ZodOptional<z.ZodBoolean>;
8
9
  model: z.ZodOptional<z.ZodString>;
9
10
  subagent_type: z.ZodOptional<z.ZodString>;
@@ -12,16 +13,18 @@ declare const inputSchema: z.ZodObject<{
12
13
  prompt: string;
13
14
  model?: string | undefined;
14
15
  description?: string | undefined;
15
- run_in_background?: boolean | undefined;
16
16
  isolated?: boolean | undefined;
17
+ isolation?: "worktree" | undefined;
18
+ run_in_background?: boolean | undefined;
17
19
  subagent_type?: string | undefined;
18
20
  allowed_tools?: string[] | undefined;
19
21
  }, {
20
22
  prompt: string;
21
23
  model?: string | undefined;
22
24
  description?: string | undefined;
23
- run_in_background?: boolean | undefined;
24
25
  isolated?: boolean | undefined;
26
+ isolation?: "worktree" | undefined;
27
+ run_in_background?: boolean | undefined;
25
28
  subagent_type?: string | undefined;
26
29
  allowed_tools?: string[] | undefined;
27
30
  }>;
@@ -1,10 +1,15 @@
1
1
  import { z } from "zod";
2
- import { createWorktree, removeWorktree, hasWorktreeChanges, isGitRepo } from "../../git/index.js";
2
+ import { createWorktree, hasWorktreeChanges, isGitRepo, removeWorktree } from "../../git/index.js";
3
3
  import { emitHook } from "../../harness/hooks.js";
4
+ import { getMessageBus } from "../../services/agent-messaging.js";
4
5
  const inputSchema = z.object({
5
6
  prompt: z.string(),
6
7
  description: z.string().optional(),
7
- isolated: z.boolean().optional(),
8
+ isolated: z.boolean().optional().describe("Whether to run in an isolated git worktree (default: true in git repos)"),
9
+ isolation: z
10
+ .enum(["worktree"])
11
+ .optional()
12
+ .describe("Isolation mode — 'worktree' creates a temporary git worktree (Claude Code compatible)"),
8
13
  run_in_background: z.boolean().optional(),
9
14
  model: z.string().optional(),
10
15
  subagent_type: z.string().optional(),
@@ -30,7 +35,9 @@ export const AgentTool = {
30
35
  }
31
36
  const { query } = await import("../../query.js");
32
37
  // Worktree isolation: create isolated copy of repo if requested or if in git repo
33
- const useWorktree = input.isolated !== false && isGitRepo(context.workingDir);
38
+ // Supports both `isolation: "worktree"` (Claude Code) and `isolated: boolean` (legacy)
39
+ const explicitWorktree = input.isolation === "worktree";
40
+ const useWorktree = (explicitWorktree || input.isolated !== false) && isGitRepo(context.workingDir);
34
41
  let worktreePath = null;
35
42
  let agentWorkingDir = context.workingDir;
36
43
  if (useWorktree) {
@@ -49,14 +56,14 @@ export const AgentTool = {
49
56
  };
50
57
  const hint = builtinHints[input.subagent_type.toLowerCase()];
51
58
  if (hint) {
52
- systemPrompt = hint + "\n\n" + systemPrompt;
59
+ systemPrompt = `${hint}\n\n${systemPrompt}`;
53
60
  }
54
61
  else {
55
62
  // Check agent roles (code-reviewer, test-writer, debugger, evaluator, etc.)
56
63
  const { getRole } = await import("../../agents/roles.js");
57
64
  role = getRole(input.subagent_type.toLowerCase());
58
65
  if (role) {
59
- systemPrompt = role.systemPromptSupplement + "\n\n" + systemPrompt;
66
+ systemPrompt = `${role.systemPromptSupplement}\n\n${systemPrompt}`;
60
67
  }
61
68
  }
62
69
  }
@@ -64,9 +71,9 @@ export const AgentTool = {
64
71
  let agentTools = context.tools;
65
72
  const allowList = input.allowed_tools ?? (role?.suggestedTools?.length ? role.suggestedTools : null);
66
73
  if (allowList) {
67
- const allowSet = new Set(allowList.map(n => n.toLowerCase()));
68
- allowSet.add('askuser'); // Always allow user communication
69
- const filtered = context.tools.filter(t => allowSet.has(t.name.toLowerCase()));
74
+ const allowSet = new Set(allowList.map((n) => n.toLowerCase()));
75
+ allowSet.add("askuser"); // Always allow user communication
76
+ const filtered = context.tools.filter((t) => allowSet.has(t.name.toLowerCase()));
70
77
  if (filtered.length > 0)
71
78
  agentTools = filtered; // Fallback to all tools if filter produces empty set
72
79
  }
@@ -82,10 +89,12 @@ export const AgentTool = {
82
89
  abortSignal: context.abortSignal,
83
90
  };
84
91
  const agentId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
85
- emitHook("subagentStart", { agentId, toolName: input.subagent_type ?? 'general' });
92
+ emitHook("subagentStart", { agentId, toolName: input.subagent_type ?? "general" });
86
93
  // Background execution: start agent and return immediately
87
94
  if (input.run_in_background) {
88
95
  const bgId = agentId;
96
+ const bus = getMessageBus();
97
+ bus.registerBackgroundAgent(bgId, input.subagent_type ?? "general");
89
98
  const runAgent = async () => {
90
99
  let finalText = "";
91
100
  const originalCwd = process.cwd();
@@ -94,7 +103,9 @@ export const AgentTool = {
94
103
  try {
95
104
  process.chdir(agentWorkingDir);
96
105
  }
97
- catch { /* ignore */ }
106
+ catch {
107
+ /* ignore */
108
+ }
98
109
  }
99
110
  for await (const event of query(input.prompt, config)) {
100
111
  if (event.type === "text_delta")
@@ -106,7 +117,9 @@ export const AgentTool = {
106
117
  try {
107
118
  process.chdir(originalCwd);
108
119
  }
109
- catch { /* ignore */ }
120
+ catch {
121
+ /* ignore */
122
+ }
110
123
  }
111
124
  // Clean up worktree only if no changes were made
112
125
  if (worktreePath) {
@@ -119,17 +132,20 @@ export const AgentTool = {
119
132
  }
120
133
  }
121
134
  }
135
+ bus.completeBackgroundAgent(bgId, finalText);
122
136
  if (context.onOutputChunk && context.callId) {
123
137
  context.onOutputChunk(context.callId, `\n[background:${bgId} completed]\n${finalText}`);
124
138
  }
125
139
  };
126
140
  runAgent().catch((err) => {
141
+ const errMsg = err instanceof Error ? err.message : String(err);
142
+ bus.errorBackgroundAgent(bgId, errMsg);
127
143
  if (context.onOutputChunk && context.callId) {
128
- context.onOutputChunk(context.callId, `\n[background:${bgId} failed: ${err instanceof Error ? err.message : String(err)}]`);
144
+ context.onOutputChunk(context.callId, `\n[background:${bgId} failed: ${errMsg}]`);
129
145
  }
130
146
  });
131
147
  return {
132
- output: `Background agent started (id: ${bgId}). You will be notified when it completes.`,
148
+ output: `Background agent started (id: ${bgId}). You will be notified when it completes. Use SendMessage with to:'${bgId}' to send it messages.`,
133
149
  isError: false,
134
150
  };
135
151
  }
@@ -142,7 +158,9 @@ export const AgentTool = {
142
158
  try {
143
159
  process.chdir(agentWorkingDir);
144
160
  }
145
- catch { /* ignore */ }
161
+ catch {
162
+ /* ignore */
163
+ }
146
164
  }
147
165
  try {
148
166
  for await (const event of query(input.prompt, config)) {
@@ -171,7 +189,9 @@ export const AgentTool = {
171
189
  try {
172
190
  process.chdir(originalCwd);
173
191
  }
174
- catch { /* ignore */ }
192
+ catch {
193
+ /* ignore */
194
+ }
175
195
  }
176
196
  }
177
197
  }
@@ -22,7 +22,7 @@ export const AskUserTool = {
22
22
  // Headless fallback — return question as text so LLM can see it
23
23
  let output = `[AskUser] ${input.question}`;
24
24
  if (input.options && input.options.length > 0) {
25
- output += "\nOptions:\n" + input.options.map((o, i) => ` ${i + 1}. ${o}`).join("\n");
25
+ output += `\nOptions:\n${input.options.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}`;
26
26
  }
27
27
  output += "\n(No interactive session available — please answer in your next message.)";
28
28
  return { output, isError: false };
@@ -7,13 +7,13 @@ declare const inputSchema: z.ZodObject<{
7
7
  run_in_background: z.ZodOptional<z.ZodBoolean>;
8
8
  }, "strip", z.ZodTypeAny, {
9
9
  command: string;
10
- description?: string | undefined;
11
10
  timeout?: number | undefined;
11
+ description?: string | undefined;
12
12
  run_in_background?: boolean | undefined;
13
13
  }, {
14
14
  command: string;
15
- description?: string | undefined;
16
15
  timeout?: number | undefined;
16
+ description?: string | undefined;
17
17
  run_in_background?: boolean | undefined;
18
18
  }>;
19
19
  export declare const BashTool: Tool<typeof inputSchema>;