@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.
- package/README.md +4 -4
- package/dist/DeferredTool.js +3 -1
- package/dist/Tool.d.ts +1 -1
- package/dist/agents/roles.js +58 -62
- package/dist/commands/cybergotchi.d.ts +1 -1
- package/dist/commands/cybergotchi.js +30 -30
- package/dist/commands/index.js +360 -122
- package/dist/components/App.d.ts +1 -1
- package/dist/components/App.js +6 -6
- package/dist/components/CompanionFooter.d.ts +1 -1
- package/dist/components/CompanionFooter.js +6 -8
- package/dist/components/CybergotchiBubble.js +5 -5
- package/dist/components/CybergotchiPanel.d.ts +1 -1
- package/dist/components/CybergotchiPanel.js +7 -7
- package/dist/components/CybergotchiPanelConnected.js +2 -2
- package/dist/components/CybergotchiSetup.js +26 -24
- package/dist/components/CybergotchiSprite.d.ts +1 -1
- package/dist/components/CybergotchiSprite.js +8 -12
- package/dist/components/DiffView.d.ts +1 -1
- package/dist/components/DiffView.js +10 -10
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/ErrorBoundary.js +1 -1
- package/dist/components/InitWizard.js +65 -33
- package/dist/components/Markdown.js +2 -4
- package/dist/components/Messages.js +4 -4
- package/dist/components/PermissionPrompt.d.ts +1 -1
- package/dist/components/PermissionPrompt.js +15 -17
- package/dist/components/REPL.d.ts +1 -1
- package/dist/components/REPL.js +74 -49
- package/dist/components/Spinner.js +2 -2
- package/dist/components/TextInput.js +35 -29
- package/dist/components/ToolCallDisplay.js +3 -5
- package/dist/cybergotchi/bones.d.ts +1 -1
- package/dist/cybergotchi/bones.js +8 -8
- package/dist/cybergotchi/config.d.ts +2 -2
- package/dist/cybergotchi/config.js +13 -13
- package/dist/cybergotchi/events.d.ts +5 -5
- package/dist/cybergotchi/events.js +7 -7
- package/dist/cybergotchi/needs.d.ts +2 -2
- package/dist/cybergotchi/needs.js +7 -9
- package/dist/cybergotchi/personality.d.ts +2 -2
- package/dist/cybergotchi/personality.js +2 -2
- package/dist/cybergotchi/species.d.ts +1 -1
- package/dist/cybergotchi/species.js +145 -217
- package/dist/cybergotchi/speech.d.ts +2 -2
- package/dist/cybergotchi/speech.js +43 -43
- package/dist/cybergotchi/types.d.ts +4 -4
- package/dist/cybergotchi/types.js +26 -26
- package/dist/cybergotchi/useCybergotchi.d.ts +1 -1
- package/dist/cybergotchi/useCybergotchi.js +29 -25
- package/dist/git/index.js +11 -9
- package/dist/harness/checkpoints.js +29 -21
- package/dist/harness/config.d.ts +12 -2
- package/dist/harness/config.js +15 -9
- package/dist/harness/context-warning.d.ts +1 -1
- package/dist/harness/context-warning.js +1 -1
- package/dist/harness/cost.js +1 -1
- package/dist/harness/credentials.js +13 -13
- package/dist/harness/hooks.js +7 -5
- package/dist/harness/keybindings.js +20 -18
- package/dist/harness/marketplace.d.ts +3 -3
- package/dist/harness/marketplace.js +55 -42
- package/dist/harness/memory.d.ts +23 -5
- package/dist/harness/memory.js +142 -41
- package/dist/harness/onboarding.js +30 -10
- package/dist/harness/plugins.d.ts +9 -1
- package/dist/harness/plugins.js +54 -30
- package/dist/harness/rules.js +12 -7
- package/dist/harness/sandbox.d.ts +34 -0
- package/dist/harness/sandbox.js +104 -0
- package/dist/harness/session-db.d.ts +55 -0
- package/dist/harness/session-db.js +165 -0
- package/dist/harness/session.d.ts +1 -1
- package/dist/harness/session.js +34 -15
- package/dist/harness/store.d.ts +3 -3
- package/dist/harness/store.js +6 -4
- package/dist/harness/submit-handler.d.ts +4 -4
- package/dist/harness/submit-handler.js +57 -21
- package/dist/harness/telemetry.d.ts +1 -1
- package/dist/harness/telemetry.js +23 -19
- package/dist/harness/traces.d.ts +2 -2
- package/dist/harness/traces.js +44 -33
- package/dist/harness/verification.d.ts +1 -1
- package/dist/harness/verification.js +50 -44
- package/dist/lsp/client.js +44 -40
- package/dist/main.js +100 -59
- package/dist/mcp/DeferredMcpTool.d.ts +4 -4
- package/dist/mcp/DeferredMcpTool.js +9 -5
- package/dist/mcp/McpTool.d.ts +4 -4
- package/dist/mcp/McpTool.js +8 -4
- package/dist/mcp/client.d.ts +2 -2
- package/dist/mcp/client.js +21 -21
- package/dist/mcp/loader.d.ts +1 -1
- package/dist/mcp/loader.js +17 -12
- package/dist/mcp/registry.d.ts +3 -3
- package/dist/mcp/registry.js +97 -97
- package/dist/mcp/schema.d.ts +1 -1
- package/dist/mcp/schema.js +16 -16
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +21 -21
- package/dist/mcp/types.d.ts +3 -3
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +10 -9
- package/dist/providers/base.d.ts +1 -1
- package/dist/providers/index.js +10 -3
- package/dist/providers/llamacpp.d.ts +2 -2
- package/dist/providers/llamacpp.js +1 -3
- package/dist/providers/ollama.d.ts +2 -2
- package/dist/providers/ollama.js +3 -4
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.js +3 -5
- package/dist/providers/openrouter.d.ts +2 -2
- package/dist/providers/router.d.ts +1 -1
- package/dist/providers/router.js +7 -7
- package/dist/query/compress.d.ts +2 -2
- package/dist/query/compress.js +22 -21
- package/dist/query/context-manager.d.ts +2 -2
- package/dist/query/context-manager.js +8 -11
- package/dist/query/errors.js +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +30 -22
- package/dist/query/tools.js +15 -12
- package/dist/query/types.d.ts +1 -1
- package/dist/query.d.ts +1 -1
- package/dist/query.js +1 -1
- package/dist/remote/auth.d.ts +2 -2
- package/dist/remote/auth.js +8 -8
- package/dist/remote/server.d.ts +3 -3
- package/dist/remote/server.js +60 -60
- package/dist/renderer/cells.js +9 -9
- package/dist/renderer/colors.js +24 -6
- package/dist/renderer/diff.d.ts +2 -2
- package/dist/renderer/diff.js +27 -19
- package/dist/renderer/differ.d.ts +1 -1
- package/dist/renderer/differ.js +9 -9
- package/dist/renderer/image.js +19 -19
- package/dist/renderer/index.d.ts +6 -6
- package/dist/renderer/index.js +163 -93
- package/dist/renderer/input.js +66 -48
- package/dist/renderer/layout.d.ts +6 -6
- package/dist/renderer/layout.js +163 -124
- package/dist/renderer/markdown.d.ts +2 -2
- package/dist/renderer/markdown.js +173 -54
- package/dist/renderer/session-browser.d.ts +2 -2
- package/dist/renderer/session-browser.js +19 -21
- package/dist/repl.d.ts +5 -5
- package/dist/repl.js +300 -198
- package/dist/sdk/index.d.ts +8 -7
- package/dist/sdk/index.js +59 -42
- package/dist/services/AgentDispatcher.d.ts +3 -3
- package/dist/services/AgentDispatcher.js +33 -29
- package/dist/services/CronExecutor.d.ts +4 -4
- package/dist/services/CronExecutor.js +12 -8
- package/dist/services/EvaluatorLoop.d.ts +3 -3
- package/dist/services/EvaluatorLoop.js +29 -21
- package/dist/services/MetaHarness.d.ts +1 -1
- package/dist/services/MetaHarness.js +41 -33
- package/dist/services/PipelineExecutor.d.ts +1 -1
- package/dist/services/PipelineExecutor.js +23 -25
- package/dist/services/SkillExtractor.d.ts +43 -0
- package/dist/services/SkillExtractor.js +143 -0
- package/dist/services/StreamingToolExecutor.d.ts +2 -2
- package/dist/services/StreamingToolExecutor.js +11 -7
- package/dist/services/a2a.d.ts +8 -8
- package/dist/services/a2a.js +44 -34
- package/dist/services/agent-messaging.d.ts +33 -15
- package/dist/services/agent-messaging.js +65 -13
- package/dist/services/cron.js +16 -16
- package/dist/tools/AgentTool/index.d.ts +5 -2
- package/dist/tools/AgentTool/index.js +35 -15
- package/dist/tools/AskUserTool/index.js +1 -1
- package/dist/tools/BashTool/index.d.ts +2 -2
- package/dist/tools/BashTool/index.js +18 -10
- package/dist/tools/CronTool/index.d.ts +2 -2
- package/dist/tools/CronTool/index.js +30 -12
- package/dist/tools/DiagnosticsTool/index.js +28 -22
- package/dist/tools/EnterPlanModeTool/index.js +93 -14
- package/dist/tools/EnterWorktreeTool/index.js +7 -3
- package/dist/tools/ExitPlanModeTool/index.d.ts +22 -1
- package/dist/tools/ExitPlanModeTool/index.js +20 -5
- package/dist/tools/ExitWorktreeTool/index.js +11 -4
- package/dist/tools/FileEditTool/index.js +3 -5
- package/dist/tools/FileReadTool/index.js +16 -10
- package/dist/tools/FileWriteTool/index.js +2 -2
- package/dist/tools/GlobTool/index.js +5 -9
- package/dist/tools/GrepTool/index.d.ts +2 -2
- package/dist/tools/GrepTool/index.js +14 -9
- package/dist/tools/ImageReadTool/index.js +2 -2
- package/dist/tools/KillProcessTool/index.js +11 -7
- package/dist/tools/LSTool/index.js +3 -3
- package/dist/tools/MemoryTool/index.d.ts +11 -11
- package/dist/tools/MemoryTool/index.js +28 -14
- package/dist/tools/MonitorTool/index.d.ts +2 -2
- package/dist/tools/MonitorTool/index.js +24 -19
- package/dist/tools/MultiEditTool/index.js +9 -5
- package/dist/tools/NotebookEditTool/index.js +3 -3
- package/dist/tools/ParallelAgentTool/index.d.ts +4 -4
- package/dist/tools/ParallelAgentTool/index.js +12 -6
- package/dist/tools/PipelineTool/index.d.ts +4 -4
- package/dist/tools/PipelineTool/index.js +3 -3
- package/dist/tools/PowerShellTool/index.js +10 -6
- package/dist/tools/RemoteTriggerTool/index.js +8 -4
- package/dist/tools/ScheduleWakeupTool/index.d.ts +42 -0
- package/dist/tools/ScheduleWakeupTool/index.js +115 -0
- package/dist/tools/SendMessageTool/index.js +25 -7
- package/dist/tools/SessionSearchTool/index.d.ts +15 -0
- package/dist/tools/SessionSearchTool/index.js +36 -0
- package/dist/tools/SkillTool/index.d.ts +3 -0
- package/dist/tools/SkillTool/index.js +39 -9
- package/dist/tools/TaskCreateTool/index.d.ts +2 -2
- package/dist/tools/TaskCreateTool/index.js +2 -2
- package/dist/tools/TaskGetTool/index.js +2 -2
- package/dist/tools/TaskListTool/index.js +3 -5
- package/dist/tools/TaskOutputTool/index.js +2 -2
- package/dist/tools/TaskStopTool/index.js +3 -3
- package/dist/tools/TaskUpdateTool/index.d.ts +4 -4
- package/dist/tools/TaskUpdateTool/index.js +2 -2
- package/dist/tools/ToolSearchTool/index.js +9 -6
- package/dist/tools/WebFetchTool/index.js +1 -1
- package/dist/tools/WebSearchTool/index.js +2 -6
- package/dist/tools.js +31 -30
- package/dist/types/permissions.js +15 -9
- package/dist/utils/bash-safety.d.ts +1 -1
- package/dist/utils/bash-safety.js +64 -54
- package/dist/utils/diff-algorithm.d.ts +3 -3
- package/dist/utils/diff-algorithm.js +7 -7
- package/dist/utils/fs.js +3 -3
- package/dist/utils/safe-env.js +1 -1
- package/dist/utils/theme-data.d.ts +1 -1
- package/dist/utils/theme-data.js +1 -1
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/tool-summary.d.ts +1 -1
- package/dist/utils/tool-summary.js +27 -9
- 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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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(
|
|
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 ===
|
|
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 —
|
|
92
|
+
// Resolve @mentions — supports @file, @file#L5-10, @file#5-10, MCP resources
|
|
73
93
|
let resolvedInput = input;
|
|
74
|
-
const mentionPattern = /@([\w][\w./-]*)
|
|
75
|
-
const mentions = [...input.matchAll(mentionPattern)]
|
|
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
|
|
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(
|
|
83
|
-
const { resolve } = await import(
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 {
|
|
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 {
|
|
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:
|
|
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,
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { readOhConfig } from
|
|
18
|
-
const TELEMETRY_DIR = join(homedir(),
|
|
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)
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
90
|
-
.split(
|
|
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(
|
|
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),
|
|
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 ===
|
|
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 ===
|
|
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 {
|
|
122
|
+
catch {
|
|
123
|
+
/* skip malformed files */
|
|
124
|
+
}
|
|
121
125
|
}
|
|
122
126
|
return { totalSessions: files.length, totalEvents, toolUsage, errorCategories };
|
|
123
127
|
}
|
package/dist/harness/traces.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type TraceSpan = {
|
|
|
15
15
|
endTime: number;
|
|
16
16
|
durationMs: number;
|
|
17
17
|
attributes: Record<string, unknown>;
|
|
18
|
-
status:
|
|
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?:
|
|
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 */
|
package/dist/harness/traces.js
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Compatible with OpenTelemetry export format.
|
|
9
9
|
*/
|
|
10
|
-
import { appendFileSync, mkdirSync,
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
const TRACE_DIR = join(homedir(),
|
|
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 =
|
|
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 ===
|
|
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)
|
|
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,
|
|
97
|
-
.split(
|
|
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(
|
|
111
|
-
.map(f => f.replace(
|
|
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
|
|
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 ===
|
|
130
|
-
const pad =
|
|
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 ===
|
|
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(
|
|
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:
|
|
156
|
-
{ key:
|
|
163
|
+
{ key: "service.name", value: { stringValue: "openharness" } },
|
|
164
|
+
{ key: "session.id", value: { stringValue: sessionId } },
|
|
157
165
|
],
|
|
158
166
|
},
|
|
159
|
-
scopeSpans: [
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 ===
|
|
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
|
|
@@ -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
|
|
12
|
-
import { existsSync } from
|
|
13
|
-
import { extname, join } from
|
|
14
|
-
import { readOhConfig } from
|
|
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,
|
|
23
|
+
if (existsSync(join(root, "tsconfig.json"))) {
|
|
24
24
|
rules.push({
|
|
25
|
-
extensions: [
|
|
26
|
-
lint:
|
|
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 = [
|
|
32
|
-
|
|
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: [
|
|
35
|
-
lint:
|
|
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,
|
|
47
|
+
if (existsSync(join(root, "pyproject.toml")) || existsSync(join(root, "setup.py"))) {
|
|
41
48
|
rules.push({
|
|
42
|
-
extensions: [
|
|
43
|
-
lint:
|
|
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,
|
|
55
|
+
if (existsSync(join(root, "go.mod"))) {
|
|
49
56
|
rules.push({
|
|
50
|
-
extensions: [
|
|
51
|
-
lint:
|
|
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,
|
|
63
|
+
if (existsSync(join(root, "Cargo.toml"))) {
|
|
57
64
|
rules.push({
|
|
58
|
-
extensions: [
|
|
59
|
-
lint:
|
|
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 ??
|
|
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:
|
|
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
|
|
104
|
-
case
|
|
110
|
+
case "Write":
|
|
111
|
+
case "Edit":
|
|
105
112
|
return toolInput.file_path ? [String(toolInput.file_path)] : [];
|
|
106
|
-
case
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
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
|
|
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:
|
|
179
|
-
stdio: [
|
|
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 ===
|
|
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 ??
|
|
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
|
}
|