@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
@@ -2,10 +2,10 @@
2
2
  * Shared submit/input handler — processes user input before sending to LLM.
3
3
  * Used by both cell renderer REPL and Ink REPL.
4
4
  */
5
- import { createUserMessage, createInfoMessage } from '../types/message.js';
6
- import { processSlashCommand } from '../commands/index.js';
7
- import { cybergotchiEvents } from '../cybergotchi/events.js';
8
- import { resolveMcpMention } from '../mcp/loader.js';
5
+ import { processSlashCommand } from "../commands/index.js";
6
+ import { cybergotchiEvents } from "../cybergotchi/events.js";
7
+ import { resolveMcpMention } from "../mcp/loader.js";
8
+ import { createInfoMessage, createUserMessage } from "../types/message.js";
9
9
  /**
10
10
  * Process user input: handle exit, companion mentions, slash commands,
11
11
  * @mentions, and prepare the prompt for the LLM.
@@ -18,16 +18,36 @@ export async function handleUserInput(input, ctx) {
18
18
  const name = ctx.companionConfig.soul.name.toLowerCase();
19
19
  const lower = trimmed.toLowerCase();
20
20
  if (lower.startsWith(`@${name}`) || lower.startsWith(`${name},`) || lower.startsWith(`${name} `)) {
21
- cybergotchiEvents.emit('cybergotchi', { type: 'userAddressed', text: trimmed });
21
+ cybergotchiEvents.emit("cybergotchi", { type: "userAddressed", text: trimmed });
22
22
  return { handled: true, messages };
23
23
  }
24
24
  }
25
+ // ! Bash mode — direct shell execution, output added to context
26
+ if (trimmed.startsWith("!") && trimmed.length > 1) {
27
+ const command = trimmed.slice(1).trim();
28
+ try {
29
+ const { execSync } = await import("node:child_process");
30
+ const output = execSync(command, {
31
+ encoding: "utf-8",
32
+ cwd: process.cwd(),
33
+ timeout: 30_000,
34
+ maxBuffer: 1024 * 1024,
35
+ windowsHide: true,
36
+ });
37
+ messages = [...messages, createInfoMessage(`$ ${command}\n${output.trimEnd()}`)];
38
+ }
39
+ catch (err) {
40
+ const output = String(err.stdout ?? err.stderr ?? err.message ?? "Command failed");
41
+ messages = [...messages, createInfoMessage(`$ ${command}\n${output.trimEnd()}`)];
42
+ }
43
+ return { handled: true, messages };
44
+ }
25
45
  // Vim toggle
26
- if (trimmed === '/vim') {
46
+ if (trimmed === "/vim") {
27
47
  return { handled: true, messages, vimToggled: true };
28
48
  }
29
49
  // Slash commands
30
- if (trimmed.startsWith('/')) {
50
+ if (trimmed.startsWith("/")) {
31
51
  const cmdCtx = {
32
52
  messages,
33
53
  model: ctx.currentModel,
@@ -69,36 +89,52 @@ export async function handleUserInput(input, ctx) {
69
89
  }
70
90
  // Normal prompt — add user message
71
91
  messages = [...messages, createUserMessage(input)];
72
- // Resolve @mentions — local files first, then MCP resources
92
+ // Resolve @mentions — supports @file, @file#L5-10, @file#5-10, MCP resources
73
93
  let resolvedInput = input;
74
- const mentionPattern = /@([\w][\w./-]*)/g;
75
- const mentions = [...input.matchAll(mentionPattern)].map(m => m[1]);
94
+ const mentionPattern = /@([\w][\w./-]*)(?:#L?(\d+)(?:-(\d+))?)?/g;
95
+ const mentions = [...input.matchAll(mentionPattern)];
76
96
  const companionName = ctx.companionConfig?.soul?.name?.toLowerCase();
77
- for (const mention of mentions) {
97
+ for (const match of mentions) {
98
+ const mention = match[1];
99
+ const startLine = match[2] ? parseInt(match[2], 10) : undefined;
100
+ const endLine = match[3] ? parseInt(match[3], 10) : startLine;
101
+ const fullRef = match[0];
78
102
  if (companionName && mention.toLowerCase() === companionName)
79
103
  continue;
80
- // Try local file first (supports paths like @src/main.ts, @README.md)
104
+ // Try local file first (supports paths like @src/main.ts, @README.md#L5-10)
81
105
  try {
82
- const { existsSync, readFileSync } = await import('node:fs');
83
- const { resolve } = await import('node:path');
106
+ const { existsSync, readFileSync } = await import("node:fs");
107
+ const { resolve } = await import("node:path");
84
108
  const filePath = resolve(process.cwd(), mention);
85
109
  if (existsSync(filePath)) {
86
- const content = readFileSync(filePath, 'utf-8');
87
- const truncated = content.length > 10_000
88
- ? content.slice(0, 10_000) + '\n[...truncated]'
89
- : content;
90
- resolvedInput += `\n\n[File @${mention}]:\n${truncated}`;
110
+ let content = readFileSync(filePath, "utf-8");
111
+ // Apply line range if specified
112
+ if (startLine !== undefined) {
113
+ const lines = content.split("\n");
114
+ const start = Math.max(0, startLine - 1); // 1-indexed to 0-indexed
115
+ const end = endLine !== undefined ? endLine : start + 1;
116
+ content = lines.slice(start, end).join("\n");
117
+ resolvedInput += `\n\n[File ${fullRef} (lines ${startLine}-${endLine ?? startLine})]:\n${content}`;
118
+ }
119
+ else {
120
+ const truncated = content.length > 10_000 ? `${content.slice(0, 10_000)}\n[...truncated]` : content;
121
+ resolvedInput += `\n\n[File @${mention}]:\n${truncated}`;
122
+ }
91
123
  continue;
92
124
  }
93
125
  }
94
- catch { /* ignore */ }
126
+ catch {
127
+ /* ignore */
128
+ }
95
129
  // Fall back to MCP resource
96
130
  try {
97
131
  const content = await resolveMcpMention(mention);
98
132
  if (content)
99
133
  resolvedInput += `\n\n[Resource @${mention}]:\n${content.slice(0, 5000)}`;
100
134
  }
101
- catch { /* ignore */ }
135
+ catch {
136
+ /* ignore */
137
+ }
102
138
  }
103
139
  return { handled: false, messages, prompt: resolvedInput };
104
140
  }
@@ -12,7 +12,7 @@
12
12
  * Optional: POST to configurable endpoint on session end.
13
13
  */
14
14
  export type TelemetryEvent = {
15
- type: 'session_start' | 'tool_call' | 'error' | 'session_end';
15
+ type: "session_start" | "tool_call" | "error" | "session_end";
16
16
  timestamp: number;
17
17
  sessionId: string;
18
18
  payload: TelemetryPayload;
@@ -11,11 +11,11 @@
11
11
  * Events are batched locally as JSONL in ~/.oh/telemetry/.
12
12
  * Optional: POST to configurable endpoint on session end.
13
13
  */
14
- import { appendFileSync, mkdirSync, existsSync, readdirSync, readFileSync } from 'node:fs';
15
- import { join } from 'node:path';
16
- import { homedir } from 'node:os';
17
- import { readOhConfig } from './config.js';
18
- const TELEMETRY_DIR = join(homedir(), '.oh', 'telemetry');
14
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { readOhConfig } from "./config.js";
18
+ const TELEMETRY_DIR = join(homedir(), ".oh", "telemetry");
19
19
  // ── State ──
20
20
  let _enabled;
21
21
  let _sessionFile = null;
@@ -40,14 +40,16 @@ export function recordEvent(event) {
40
40
  return;
41
41
  try {
42
42
  const file = getSessionFile(event.sessionId);
43
- appendFileSync(file, JSON.stringify(event) + '\n');
43
+ appendFileSync(file, `${JSON.stringify(event)}\n`);
44
+ }
45
+ catch {
46
+ /* never crash on telemetry failure */
44
47
  }
45
- catch { /* never crash on telemetry failure */ }
46
48
  }
47
49
  /** Convenience: record a tool call event */
48
50
  export function recordToolCall(sessionId, toolName, durationMs, isError) {
49
51
  recordEvent({
50
- type: 'tool_call',
52
+ type: "tool_call",
51
53
  timestamp: Date.now(),
52
54
  sessionId,
53
55
  payload: { toolName, durationMs, isError },
@@ -56,7 +58,7 @@ export function recordToolCall(sessionId, toolName, durationMs, isError) {
56
58
  /** Convenience: record session start */
57
59
  export function recordSessionStart(sessionId, provider, model) {
58
60
  recordEvent({
59
- type: 'session_start',
61
+ type: "session_start",
60
62
  timestamp: Date.now(),
61
63
  sessionId,
62
64
  payload: { provider, model, platform: process.platform },
@@ -65,7 +67,7 @@ export function recordSessionStart(sessionId, provider, model) {
65
67
  /** Convenience: record session end with stats */
66
68
  export function recordSessionEnd(sessionId, stats) {
67
69
  recordEvent({
68
- type: 'session_end',
70
+ type: "session_end",
69
71
  timestamp: Date.now(),
70
72
  sessionId,
71
73
  payload: stats,
@@ -74,7 +76,7 @@ export function recordSessionEnd(sessionId, stats) {
74
76
  /** Convenience: record an error */
75
77
  export function recordError(sessionId, category) {
76
78
  recordEvent({
77
- type: 'error',
79
+ type: "error",
78
80
  timestamp: Date.now(),
79
81
  sessionId,
80
82
  payload: { errorCategory: category },
@@ -86,10 +88,10 @@ export function readSessionEvents(sessionId) {
86
88
  if (!existsSync(file))
87
89
  return [];
88
90
  try {
89
- return readFileSync(file, 'utf-8')
90
- .split('\n')
91
+ return readFileSync(file, "utf-8")
92
+ .split("\n")
91
93
  .filter(Boolean)
92
- .map(line => JSON.parse(line));
94
+ .map((line) => JSON.parse(line));
93
95
  }
94
96
  catch {
95
97
  return [];
@@ -99,25 +101,27 @@ export function readSessionEvents(sessionId) {
99
101
  export function getAggregateStats() {
100
102
  if (!existsSync(TELEMETRY_DIR))
101
103
  return { totalSessions: 0, totalEvents: 0, toolUsage: {}, errorCategories: {} };
102
- const files = readdirSync(TELEMETRY_DIR).filter(f => f.endsWith('.jsonl'));
104
+ const files = readdirSync(TELEMETRY_DIR).filter((f) => f.endsWith(".jsonl"));
103
105
  const toolUsage = {};
104
106
  const errorCategories = {};
105
107
  let totalEvents = 0;
106
108
  for (const file of files) {
107
109
  try {
108
- const lines = readFileSync(join(TELEMETRY_DIR, file), 'utf-8').split('\n').filter(Boolean);
110
+ const lines = readFileSync(join(TELEMETRY_DIR, file), "utf-8").split("\n").filter(Boolean);
109
111
  totalEvents += lines.length;
110
112
  for (const line of lines) {
111
113
  const event = JSON.parse(line);
112
- if (event.type === 'tool_call' && event.payload.toolName) {
114
+ if (event.type === "tool_call" && event.payload.toolName) {
113
115
  toolUsage[event.payload.toolName] = (toolUsage[event.payload.toolName] ?? 0) + 1;
114
116
  }
115
- if (event.type === 'error' && event.payload.errorCategory) {
117
+ if (event.type === "error" && event.payload.errorCategory) {
116
118
  errorCategories[event.payload.errorCategory] = (errorCategories[event.payload.errorCategory] ?? 0) + 1;
117
119
  }
118
120
  }
119
121
  }
120
- catch { /* skip malformed files */ }
122
+ catch {
123
+ /* skip malformed files */
124
+ }
121
125
  }
122
126
  return { totalSessions: files.length, totalEvents, toolUsage, errorCategories };
123
127
  }
@@ -15,7 +15,7 @@ export type TraceSpan = {
15
15
  endTime: number;
16
16
  durationMs: number;
17
17
  attributes: Record<string, unknown>;
18
- status: 'ok' | 'error';
18
+ status: "ok" | "error";
19
19
  };
20
20
  export type TraceEvent = {
21
21
  name: string;
@@ -31,7 +31,7 @@ export declare class SessionTracer {
31
31
  /** Start a new span. Returns the span ID. */
32
32
  startSpan(name: string, attributes?: Record<string, unknown>, parentSpanId?: string): string;
33
33
  /** End a span and record it. */
34
- endSpan(spanId: string, status?: 'ok' | 'error', extraAttributes?: Record<string, unknown>): TraceSpan | null;
34
+ endSpan(spanId: string, status?: "ok" | "error", extraAttributes?: Record<string, unknown>): TraceSpan | null;
35
35
  /** Get all completed spans */
36
36
  getSpans(): TraceSpan[];
37
37
  /** Get a summary of the trace */
@@ -7,11 +7,12 @@
7
7
  *
8
8
  * Compatible with OpenTelemetry export format.
9
9
  */
10
- import { appendFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'node:fs';
11
- import { join } from 'node:path';
12
- import { homedir } from 'node:os';
13
- const TRACE_DIR = join(homedir(), '.oh', 'traces');
10
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ const TRACE_DIR = join(homedir(), ".oh", "traces");
14
14
  // ── Tracer ──
15
+ const MAX_IN_MEMORY_SPANS = 1000;
15
16
  export class SessionTracer {
16
17
  sessionId;
17
18
  spans = [];
@@ -27,7 +28,7 @@ export class SessionTracer {
27
28
  return spanId;
28
29
  }
29
30
  /** End a span and record it. */
30
- endSpan(spanId, status = 'ok', extraAttributes) {
31
+ endSpan(spanId, status = "ok", extraAttributes) {
31
32
  const active = this.activeSpans.get(spanId);
32
33
  if (!active)
33
34
  return null;
@@ -44,6 +45,10 @@ export class SessionTracer {
44
45
  status,
45
46
  };
46
47
  this.spans.push(span);
48
+ // Cap in-memory spans (durable source is on disk)
49
+ if (this.spans.length > MAX_IN_MEMORY_SPANS) {
50
+ this.spans = this.spans.slice(-MAX_IN_MEMORY_SPANS);
51
+ }
47
52
  this.persistSpan(span);
48
53
  return span;
49
54
  }
@@ -62,7 +67,7 @@ export class SessionTracer {
62
67
  entry.count++;
63
68
  entry.totalMs += span.durationMs;
64
69
  spansByName[span.name] = entry;
65
- if (span.status === 'error')
70
+ if (span.status === "error")
66
71
  errors++;
67
72
  if (span.startTime < minStart)
68
73
  minStart = span.startTime;
@@ -81,9 +86,11 @@ export class SessionTracer {
81
86
  try {
82
87
  mkdirSync(TRACE_DIR, { recursive: true });
83
88
  const file = join(TRACE_DIR, `${this.sessionId}.jsonl`);
84
- appendFileSync(file, JSON.stringify(span) + '\n');
89
+ appendFileSync(file, `${JSON.stringify(span)}\n`);
90
+ }
91
+ catch {
92
+ /* never crash on tracing failure */
85
93
  }
86
- catch { /* never crash on tracing failure */ }
87
94
  }
88
95
  }
89
96
  // ── Trace Loading ──
@@ -93,10 +100,10 @@ export function loadTrace(sessionId) {
93
100
  if (!existsSync(file))
94
101
  return [];
95
102
  try {
96
- return readFileSync(file, 'utf-8')
97
- .split('\n')
103
+ return readFileSync(file, "utf-8")
104
+ .split("\n")
98
105
  .filter(Boolean)
99
- .map(line => JSON.parse(line));
106
+ .map((line) => JSON.parse(line));
100
107
  }
101
108
  catch {
102
109
  return [];
@@ -107,16 +114,16 @@ export function listTracedSessions() {
107
114
  if (!existsSync(TRACE_DIR))
108
115
  return [];
109
116
  return readdirSync(TRACE_DIR)
110
- .filter(f => f.endsWith('.jsonl'))
111
- .map(f => f.replace('.jsonl', ''));
117
+ .filter((f) => f.endsWith(".jsonl"))
118
+ .map((f) => f.replace(".jsonl", ""));
112
119
  }
113
120
  /** Format trace for display */
114
121
  export function formatTrace(spans) {
115
122
  if (spans.length === 0)
116
- return 'No trace spans recorded.';
123
+ return "No trace spans recorded.";
117
124
  const lines = [`Trace (${spans.length} spans):\n`];
118
125
  // Group by parent for tree display
119
- const roots = spans.filter(s => !s.parentSpanId);
126
+ const roots = spans.filter((s) => !s.parentSpanId);
120
127
  const children = new Map();
121
128
  for (const s of spans) {
122
129
  if (s.parentSpanId) {
@@ -126,12 +133,12 @@ export function formatTrace(spans) {
126
133
  }
127
134
  }
128
135
  function renderSpan(span, indent) {
129
- const status = span.status === 'error' ? '' : '';
130
- const pad = ' '.repeat(indent);
136
+ const status = span.status === "error" ? "" : "";
137
+ const pad = " ".repeat(indent);
131
138
  const attrs = Object.entries(span.attributes)
132
139
  .filter(([, v]) => v !== undefined)
133
140
  .map(([k, v]) => `${k}=${String(v).slice(0, 30)}`)
134
- .join(' ');
141
+ .join(" ");
135
142
  lines.push(`${pad}${status} ${span.name} (${span.durationMs}ms) ${attrs}`);
136
143
  const kids = children.get(span.spanId) ?? [];
137
144
  for (const kid of kids)
@@ -141,27 +148,29 @@ export function formatTrace(spans) {
141
148
  renderSpan(root, 0);
142
149
  // Summary
143
150
  const totalMs = spans.reduce((sum, s) => sum + s.durationMs, 0);
144
- const errors = spans.filter(s => s.status === 'error').length;
145
- lines.push('');
151
+ const errors = spans.filter((s) => s.status === "error").length;
152
+ lines.push("");
146
153
  lines.push(`Total: ${spans.length} spans, ${totalMs}ms, ${errors} errors`);
147
- return lines.join('\n');
154
+ return lines.join("\n");
148
155
  }
149
156
  /** Export trace in OpenTelemetry-compatible format */
150
157
  export function exportTraceOTLP(sessionId, spans) {
151
158
  return {
152
- resourceSpans: [{
159
+ resourceSpans: [
160
+ {
153
161
  resource: {
154
162
  attributes: [
155
- { key: 'service.name', value: { stringValue: 'openharness' } },
156
- { key: 'session.id', value: { stringValue: sessionId } },
163
+ { key: "service.name", value: { stringValue: "openharness" } },
164
+ { key: "session.id", value: { stringValue: sessionId } },
157
165
  ],
158
166
  },
159
- scopeSpans: [{
160
- scope: { name: 'openharness.agent' },
161
- spans: spans.map(s => ({
162
- traceId: sessionId.padEnd(32, '0').slice(0, 32),
163
- spanId: s.spanId.padEnd(16, '0').slice(0, 16),
164
- parentSpanId: s.parentSpanId?.padEnd(16, '0').slice(0, 16),
167
+ scopeSpans: [
168
+ {
169
+ scope: { name: "openharness.agent" },
170
+ spans: spans.map((s) => ({
171
+ traceId: sessionId.padEnd(32, "0").slice(0, 32),
172
+ spanId: s.spanId.padEnd(16, "0").slice(0, 16),
173
+ parentSpanId: s.parentSpanId?.padEnd(16, "0").slice(0, 16),
165
174
  name: s.name,
166
175
  startTimeUnixNano: s.startTime * 1_000_000,
167
176
  endTimeUnixNano: s.endTime * 1_000_000,
@@ -169,10 +178,12 @@ export function exportTraceOTLP(sessionId, spans) {
169
178
  key: k,
170
179
  value: { stringValue: String(v) },
171
180
  })),
172
- status: { code: s.status === 'ok' ? 1 : 2 },
181
+ status: { code: s.status === "ok" ? 1 : 2 },
173
182
  })),
174
- }],
175
- }],
183
+ },
184
+ ],
185
+ },
186
+ ],
176
187
  };
177
188
  }
178
189
  //# sourceMappingURL=traces.js.map
@@ -15,7 +15,7 @@ export type VerificationRule = {
15
15
  };
16
16
  export type VerificationConfig = {
17
17
  enabled: boolean;
18
- mode: 'warn' | 'block';
18
+ mode: "warn" | "block";
19
19
  rules: VerificationRule[];
20
20
  };
21
21
  export type VerificationResult = {
@@ -8,10 +8,10 @@
8
8
  * This is the single highest-impact harness engineering pattern —
9
9
  * research shows 2-3x quality improvement from automated feedback.
10
10
  */
11
- import { execSync } from 'node:child_process';
12
- import { existsSync } from 'node:fs';
13
- import { extname, join } from 'node:path';
14
- import { readOhConfig } from './config.js';
11
+ import { execSync } from "node:child_process";
12
+ import { existsSync } from "node:fs";
13
+ import { extname, join } from "node:path";
14
+ import { readOhConfig } from "./config.js";
15
15
  const MAX_SUMMARY_CHARS = 500;
16
16
  const DEFAULT_TIMEOUT_MS = 10_000;
17
17
  // ── Auto-detection ──
@@ -20,43 +20,50 @@ export function autoDetectRules(projectRoot) {
20
20
  const root = projectRoot ?? process.cwd();
21
21
  const rules = [];
22
22
  // TypeScript
23
- if (existsSync(join(root, 'tsconfig.json'))) {
23
+ if (existsSync(join(root, "tsconfig.json"))) {
24
24
  rules.push({
25
- extensions: ['.ts', '.tsx'],
26
- lint: 'npx tsc --noEmit 2>&1 | head -20',
25
+ extensions: [".ts", ".tsx"],
26
+ lint: "npx tsc --noEmit 2>&1 | head -20",
27
27
  timeout: 15_000,
28
28
  });
29
29
  }
30
30
  // ESLint (JS/TS)
31
- const eslintConfigs = ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', 'eslint.config.js', 'eslint.config.mjs'];
32
- if (eslintConfigs.some(f => existsSync(join(root, f)))) {
31
+ const eslintConfigs = [
32
+ ".eslintrc",
33
+ ".eslintrc.js",
34
+ ".eslintrc.json",
35
+ ".eslintrc.yml",
36
+ "eslint.config.js",
37
+ "eslint.config.mjs",
38
+ ];
39
+ if (eslintConfigs.some((f) => existsSync(join(root, f)))) {
33
40
  rules.push({
34
- extensions: ['.js', '.jsx', '.ts', '.tsx'],
35
- lint: 'npx eslint {file} --no-color 2>&1 | head -15',
41
+ extensions: [".js", ".jsx", ".ts", ".tsx"],
42
+ lint: "npx eslint {file} --no-color 2>&1 | head -15",
36
43
  timeout: 10_000,
37
44
  });
38
45
  }
39
46
  // Python — ruff (fast) or pylint
40
- if (existsSync(join(root, 'pyproject.toml')) || existsSync(join(root, 'setup.py'))) {
47
+ if (existsSync(join(root, "pyproject.toml")) || existsSync(join(root, "setup.py"))) {
41
48
  rules.push({
42
- extensions: ['.py'],
43
- lint: 'ruff check {file} 2>&1 | head -10',
49
+ extensions: [".py"],
50
+ lint: "ruff check {file} 2>&1 | head -10",
44
51
  timeout: 10_000,
45
52
  });
46
53
  }
47
54
  // Go
48
- if (existsSync(join(root, 'go.mod'))) {
55
+ if (existsSync(join(root, "go.mod"))) {
49
56
  rules.push({
50
- extensions: ['.go'],
51
- lint: 'go vet ./... 2>&1 | head -10',
57
+ extensions: [".go"],
58
+ lint: "go vet ./... 2>&1 | head -10",
52
59
  timeout: 15_000,
53
60
  });
54
61
  }
55
62
  // Rust
56
- if (existsSync(join(root, 'Cargo.toml'))) {
63
+ if (existsSync(join(root, "Cargo.toml"))) {
57
64
  rules.push({
58
- extensions: ['.rs'],
59
- lint: 'cargo check 2>&1 | tail -10',
65
+ extensions: [".rs"],
66
+ lint: "cargo check 2>&1 | tail -10",
60
67
  timeout: 30_000,
61
68
  });
62
69
  }
@@ -78,7 +85,7 @@ export function getVerificationConfig() {
78
85
  }
79
86
  _cachedConfig = {
80
87
  enabled: true,
81
- mode: v.mode ?? 'warn',
88
+ mode: v.mode ?? "warn",
82
89
  rules: v.rules ?? autoDetectRules(),
83
90
  };
84
91
  return _cachedConfig;
@@ -89,7 +96,7 @@ export function getVerificationConfig() {
89
96
  _cachedConfig = null;
90
97
  return null;
91
98
  }
92
- _cachedConfig = { enabled: true, mode: 'warn', rules: autoRules };
99
+ _cachedConfig = { enabled: true, mode: "warn", rules: autoRules };
93
100
  return _cachedConfig;
94
101
  }
95
102
  /** Clear cached config (for testing or after config changes) */
@@ -100,15 +107,15 @@ export function invalidateVerificationCache() {
100
107
  /** Extract file paths from tool input that were modified */
101
108
  export function extractFilePaths(toolName, toolInput) {
102
109
  switch (toolName) {
103
- case 'Write':
104
- case 'Edit':
110
+ case "Write":
111
+ case "Edit":
105
112
  return toolInput.file_path ? [String(toolInput.file_path)] : [];
106
- case 'MultiEdit':
113
+ case "MultiEdit":
107
114
  // MultiEdit has an array of edits, each with file_path
108
115
  if (Array.isArray(toolInput.edits)) {
109
116
  const paths = new Set();
110
117
  for (const edit of toolInput.edits) {
111
- if (edit && typeof edit === 'object' && 'file_path' in edit) {
118
+ if (edit && typeof edit === "object" && "file_path" in edit) {
112
119
  paths.add(String(edit.file_path));
113
120
  }
114
121
  }
@@ -123,7 +130,7 @@ export function extractFilePaths(toolName, toolInput) {
123
130
  /** Find the matching rule for a file extension */
124
131
  function findRule(filePath, rules) {
125
132
  const ext = extname(filePath).toLowerCase();
126
- return rules.find(r => r.extensions.includes(ext)) ?? null;
133
+ return rules.find((r) => r.extensions.includes(ext)) ?? null;
127
134
  }
128
135
  /**
129
136
  * Shell-escape a file path to prevent command injection.
@@ -131,7 +138,7 @@ function findRule(filePath, rules) {
131
138
  */
132
139
  function shellEscape(s) {
133
140
  // On Windows, use double quotes; on POSIX, use single quotes
134
- if (process.platform === 'win32') {
141
+ if (process.platform === "win32") {
135
142
  // Double-quote and escape internal double quotes and special chars
136
143
  return `"${s.replace(/"/g, '\\"')}"`;
137
144
  }
@@ -144,30 +151,30 @@ function shellEscape(s) {
144
151
  */
145
152
  export async function runVerificationForFiles(filePaths, config) {
146
153
  if (filePaths.length === 0)
147
- return { ran: false, passed: true, summary: '' };
154
+ return { ran: false, passed: true, summary: "" };
148
155
  if (filePaths.length === 1)
149
156
  return runVerification(filePaths[0], config);
150
157
  const results = [];
151
158
  for (const fp of filePaths) {
152
159
  results.push(await runVerification(fp, config));
153
160
  }
154
- const ran = results.some(r => r.ran);
155
- const passed = results.every(r => r.passed);
156
- const failures = results.filter(r => r.ran && !r.passed);
161
+ const ran = results.some((r) => r.ran);
162
+ const passed = results.every((r) => r.passed);
163
+ const failures = results.filter((r) => r.ran && !r.passed);
157
164
  if (!ran)
158
- return { ran: false, passed: true, summary: '' };
165
+ return { ran: false, passed: true, summary: "" };
159
166
  if (passed)
160
- return { ran: true, passed: true, summary: '' };
167
+ return { ran: true, passed: true, summary: "" };
161
168
  // Aggregate failure summaries (cap total to MAX_SUMMARY_CHARS)
162
- const summaryParts = failures.map(r => r.summary).filter(Boolean);
163
- const summary = summaryParts.join('\n---\n').slice(0, MAX_SUMMARY_CHARS);
169
+ const summaryParts = failures.map((r) => r.summary).filter(Boolean);
170
+ const summary = summaryParts.join("\n---\n").slice(0, MAX_SUMMARY_CHARS);
164
171
  return { ran: true, passed: false, summary };
165
172
  }
166
173
  /** Run verification for a single file. Returns result with concise summary. */
167
174
  export async function runVerification(filePath, config) {
168
175
  const rule = findRule(filePath, config.rules);
169
- if (!rule || !rule.lint) {
170
- return { ran: false, passed: true, summary: '' };
176
+ if (!rule?.lint) {
177
+ return { ran: false, passed: true, summary: "" };
171
178
  }
172
179
  const command = rule.lint.replace(/\{file\}/g, shellEscape(filePath));
173
180
  const timeout = rule.timeout ?? DEFAULT_TIMEOUT_MS;
@@ -175,21 +182,20 @@ export async function runVerification(filePath, config) {
175
182
  execSync(command, {
176
183
  timeout,
177
184
  cwd: process.cwd(),
178
- encoding: 'utf-8',
179
- stdio: ['pipe', 'pipe', 'pipe'],
185
+ encoding: "utf-8",
186
+ stdio: ["pipe", "pipe", "pipe"],
180
187
  windowsHide: true,
181
188
  });
182
189
  // Exit code 0 = passed
183
- return { ran: true, passed: true, summary: '' };
190
+ return { ran: true, passed: true, summary: "" };
184
191
  }
185
192
  catch (err) {
186
193
  // Timeout detection — check killed flag, signal, or error code
187
- const isTimeout = err.killed || err.signal === 'SIGTERM' || err.code === 'ETIMEDOUT'
188
- || (err.status === null && err.signal);
194
+ const isTimeout = err.killed || err.signal === "SIGTERM" || err.code === "ETIMEDOUT" || (err.status === null && err.signal);
189
195
  if (isTimeout) {
190
196
  return { ran: true, passed: false, summary: `Verification timed out after ${timeout / 1000}s` };
191
197
  }
192
- const output = String(err.stdout ?? err.stderr ?? err.message ?? 'Unknown error');
198
+ const output = String(err.stdout ?? err.stderr ?? err.message ?? "Unknown error");
193
199
  const summary = output.slice(0, MAX_SUMMARY_CHARS).trim();
194
200
  return { ran: true, passed: false, summary };
195
201
  }