@zhijiewang/openharness 2.12.0 → 2.14.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.
@@ -2,7 +2,7 @@
2
2
  * Tool execution — permission checking, batching, output capping.
3
3
  */
4
4
  import { createCheckpoint, getAffectedFiles } from "../harness/checkpoints.js";
5
- import { emitHook } from "../harness/hooks.js";
5
+ import { emitHook, emitHookWithOutcome } from "../harness/hooks.js";
6
6
  import { findToolByName } from "../Tool.js";
7
7
  import { createToolResultMessage } from "../types/message.js";
8
8
  import { checkPermission } from "../types/permissions.js";
@@ -45,9 +45,28 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
45
45
  if (perm.reason === "needs-approval" && askUser) {
46
46
  const { formatToolArgs } = await import("../utils/tool-summary.js");
47
47
  const description = formatToolArgs(tool.name, toolCall.arguments);
48
- const allowed = await askUser(tool.name, description, tool.riskLevel);
49
- if (!allowed) {
50
- return { output: "Permission denied by user.", isError: true };
48
+ // Hook: permissionRequest fires between preToolUse and the interactive askUser prompt.
49
+ // Only fires when checkPermission says "needs-approval" AND askUser is provided.
50
+ const hookOutcome = await emitHookWithOutcome("permissionRequest", {
51
+ toolName: tool.name,
52
+ toolArgs: JSON.stringify(toolCall.arguments).slice(0, 1000),
53
+ toolInputJson: JSON.stringify(parsed.data).slice(0, 1000),
54
+ permissionMode,
55
+ permissionAction: "ask",
56
+ });
57
+ if (hookOutcome.permissionDecision === "allow") {
58
+ // Hook granted permission — skip interactive prompt and proceed to execution.
59
+ }
60
+ else if (hookOutcome.permissionDecision === "deny" || !hookOutcome.allowed) {
61
+ const reason = hookOutcome.reason ? `: ${hookOutcome.reason}` : "";
62
+ return { output: `Permission denied by hook${reason}`, isError: true };
63
+ }
64
+ else {
65
+ // "ask" or no decision → fall through to interactive prompt
66
+ const allowed = await askUser(tool.name, description, tool.riskLevel);
67
+ if (!allowed) {
68
+ return { output: "Permission denied by user.", isError: true };
69
+ }
51
70
  }
52
71
  }
53
72
  else {
@@ -79,12 +98,23 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
79
98
  toolAbort.addEventListener("abort", () => reject(new Error(`Tool '${tool.name}' timed out after ${TOOL_TIMEOUT_MS / 1000}s`)));
80
99
  }),
81
100
  ]);
82
- // Hook: postToolUse
83
- emitHook("postToolUse", {
84
- toolName: tool.name,
85
- toolArgs: JSON.stringify(toolCall.arguments).slice(0, 1000),
86
- toolOutput: result.output.slice(0, 1000),
87
- });
101
+ // Hook: postToolUse / postToolUseFailure (mutually exclusive — strict CC parity)
102
+ if (result.isError) {
103
+ emitHook("postToolUseFailure", {
104
+ toolName: tool.name,
105
+ toolArgs: JSON.stringify(toolCall.arguments).slice(0, 1000),
106
+ toolOutput: result.output.slice(0, 1000),
107
+ toolError: "ReportedError",
108
+ errorMessage: result.output.slice(0, 1000),
109
+ });
110
+ }
111
+ else {
112
+ emitHook("postToolUse", {
113
+ toolName: tool.name,
114
+ toolArgs: JSON.stringify(toolCall.arguments).slice(0, 1000),
115
+ toolOutput: result.output.slice(0, 1000),
116
+ });
117
+ }
88
118
  // Emit fileChanged hook for file-modifying tools
89
119
  if (!result.isError && ["Edit", "Write", "MultiEdit"].includes(tool.name)) {
90
120
  const filePaths = getAffectedFiles(tool.name, parsed.data);
@@ -141,7 +171,15 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
141
171
  return { output, isError: result.isError };
142
172
  }
143
173
  catch (err) {
144
- return { output: `Tool error: ${err instanceof Error ? err.message : String(err)}`, isError: true };
174
+ const errMsg = err instanceof Error ? err.message : String(err);
175
+ const errName = err instanceof Error ? err.name : "ExecutionError";
176
+ emitHook("postToolUseFailure", {
177
+ toolName: tool.name,
178
+ toolArgs: JSON.stringify(toolCall.arguments).slice(0, 1000),
179
+ errorMessage: errMsg,
180
+ toolError: errName,
181
+ });
182
+ return { output: `Tool error: ${errMsg}`, isError: true };
145
183
  }
146
184
  }
147
185
  export async function* executeToolCalls(toolCalls, tools, context, permissionMode, askUser, state) {
@@ -20,6 +20,8 @@ export type QueryConfig = {
20
20
  workingDir?: string;
21
21
  /** Auto-commit after each file-modifying tool */
22
22
  gitCommitPerTool?: boolean;
23
+ /** For sub-agent invocations: the agent role name (feeds into the model router). */
24
+ role?: string;
23
25
  };
24
26
  export type TransitionReason = "next_turn" | "retry_network" | "retry_prompt_too_long" | "retry_max_output_tokens";
25
27
  export type QueryLoopState = {
@@ -33,5 +35,9 @@ export type QueryLoopState = {
33
35
  promptTooLongRetries?: number;
34
36
  /** Track consecutive compression failures for circuit breaker */
35
37
  compressionFailures?: number;
38
+ /** Whether the previous turn made any tool calls (feeds ModelRouter) */
39
+ lastTurnHadTools?: boolean;
40
+ /** Number of tool calls in the previous turn (feeds ModelRouter) */
41
+ lastTurnToolCount?: number;
36
42
  };
37
43
  //# sourceMappingURL=types.d.ts.map
@@ -99,7 +99,7 @@ export const AgentTool = {
99
99
  const runAgent = async () => {
100
100
  let finalText = "";
101
101
  try {
102
- for await (const event of query(input.prompt, config)) {
102
+ for await (const event of query(input.prompt, { ...config, role: role?.id })) {
103
103
  if (event.type === "text_delta")
104
104
  finalText += event.content;
105
105
  }
@@ -137,7 +137,7 @@ export const AgentTool = {
137
137
  let finalText = "";
138
138
  try {
139
139
  try {
140
- for await (const event of query(input.prompt, config)) {
140
+ for await (const event of query(input.prompt, { ...config, role: role?.id })) {
141
141
  if (event.type === "text_delta") {
142
142
  finalText += event.content;
143
143
  }
@@ -5,12 +5,12 @@ declare const inputSchema: z.ZodObject<{
5
5
  reason: z.ZodString;
6
6
  prompt: z.ZodString;
7
7
  }, "strip", z.ZodTypeAny, {
8
- prompt: string;
9
8
  reason: string;
9
+ prompt: string;
10
10
  delaySeconds: number;
11
11
  }, {
12
- prompt: string;
13
12
  reason: string;
13
+ prompt: string;
14
14
  delaySeconds: number;
15
15
  }>;
16
16
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -84,5 +84,8 @@
84
84
  "bugs": {
85
85
  "url": "https://github.com/zhijiewong/openharness/issues"
86
86
  },
87
- "homepage": "https://github.com/zhijiewong/openharness#readme"
87
+ "homepage": "https://github.com/zhijiewong/openharness#readme",
88
+ "optionalDependencies": {
89
+ "@napi-rs/keyring": "^1.2.0"
90
+ }
88
91
  }