@zhijiewang/openharness 2.20.0 → 2.22.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.
@@ -22,6 +22,16 @@ export type QueryConfig = {
22
22
  gitCommitPerTool?: boolean;
23
23
  /** For sub-agent invocations: the agent role name (feeds into the model router). */
24
24
  role?: string;
25
+ /**
26
+ * MCP tool name (e.g. `mcp__myperm__check`) consulted when a tool needs
27
+ * approval and no permission hook gave a decision (audit B1). Mirrors
28
+ * Claude Code's `--permission-prompt-tool`. The tool is invoked with
29
+ * `{ tool_name, input }` and is expected to return a JSON string with
30
+ * shape `{ "behavior": "allow" | "deny", "message"?: string }`. Falls
31
+ * through to the interactive `askUser` prompt (or headless deny) when
32
+ * the tool is missing, throws, or returns malformed JSON.
33
+ */
34
+ permissionPromptTool?: string;
25
35
  };
26
36
  export type TransitionReason = "next_turn" | "retry_network" | "retry_prompt_too_long" | "retry_max_output_tokens";
27
37
  export type QueryLoopState = {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { createWorktree, isGitRepo } from "../../git/index.js";
3
+ import { emitHook } from "../../harness/hooks.js";
3
4
  const inputSchema = z.object({
4
5
  branch: z.string().optional().describe("Branch name for the worktree (auto-generated if omitted)"),
5
6
  });
@@ -22,6 +23,9 @@ export const EnterWorktreeTool = {
22
23
  if (!path) {
23
24
  return { output: "Failed to create worktree.", isError: true };
24
25
  }
26
+ // Symmetric to taskCreated — fire only on the success path so audit hooks
27
+ // can react to the new worktree (e.g. set up a per-worktree scratch dir).
28
+ emitHook("worktreeCreate", { worktreePath: path, worktreeParent: context.workingDir });
25
29
  return { output: `Worktree created at: ${path}\nUse ExitWorktree to clean up when done.`, isError: false };
26
30
  },
27
31
  prompt() {
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { hasWorktreeChanges, removeWorktree } from "../../git/index.js";
3
+ import { emitHook } from "../../harness/hooks.js";
3
4
  const inputSchema = z.object({
4
5
  path: z.string().describe("Path to the worktree to remove"),
5
6
  force: z.boolean().optional().describe("Force removal even with uncommitted changes"),
@@ -24,6 +25,12 @@ export const ExitWorktreeTool = {
24
25
  }
25
26
  try {
26
27
  removeWorktree(input.path);
28
+ // Fire after removeWorktree resolves so the hook only sees confirmed
29
+ // removals — symmetric to worktreeCreate firing on success.
30
+ emitHook("worktreeRemove", {
31
+ worktreePath: input.path,
32
+ worktreeForced: input.force ? "true" : "false",
33
+ });
27
34
  return { output: `Worktree removed: ${input.path}`, isError: false };
28
35
  }
29
36
  catch (err) {
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Categorized debug logger — gates verbose internal traces behind a runtime
3
+ * switch so they're silent by default but easy to flip on for support / CI.
4
+ *
5
+ * Activation precedence (highest first):
6
+ * 1. `configureDebug({ categories })` from a CLI flag (`--debug [cats]`)
7
+ * 2. `OH_DEBUG` env var
8
+ *
9
+ * Sink precedence:
10
+ * 1. `configureDebug({ file })` from `--debug-file <path>`
11
+ * 2. `OH_DEBUG_FILE` env var
12
+ * 3. `process.stderr` (default)
13
+ *
14
+ * Categories are arbitrary strings — call sites pick them. The CLI accepts a
15
+ * comma-separated list (`--debug mcp,hooks`) or `--debug` alone for "all".
16
+ *
17
+ * Wire pattern:
18
+ * import { configureDebug, debug } from "./utils/debug.js";
19
+ * configureDebug({ categories: opts.debug, file: opts.debugFile });
20
+ * debug("mcp", "connected", server.name);
21
+ */
22
+ /**
23
+ * Parse the raw flag value into a Set of enabled categories.
24
+ *
25
+ * Accepted values:
26
+ * - `undefined` / empty / `false` → no debug
27
+ * - `true` / `"*"` / `"all"` / `"1"` → all categories
28
+ * - `"mcp,hooks,provider"` → comma-separated explicit list
29
+ *
30
+ * Whitespace is trimmed and empty entries dropped, so `"mcp, ,hooks"` is
31
+ * equivalent to `"mcp,hooks"`. Pure function — exposed for testability.
32
+ */
33
+ export declare function parseDebugCategories(raw: string | boolean | undefined): Set<string>;
34
+ export interface ConfigureDebugOptions {
35
+ /** CLI flag value: `--debug` → true, `--debug mcp` → "mcp", absent → undefined. */
36
+ categories?: string | boolean | undefined;
37
+ /** CLI flag value: `--debug-file <path>` — appended to, never truncated. */
38
+ file?: string;
39
+ /** Test injection — overrides the file/stderr sink. Not used at runtime. */
40
+ sink?: NodeJS.WritableStream;
41
+ }
42
+ /**
43
+ * Apply debug configuration. Safe to call multiple times — later calls fully
44
+ * replace earlier state. When `categories` is undefined, falls back to
45
+ * `OH_DEBUG`; when `file` is undefined, falls back to `OH_DEBUG_FILE`.
46
+ *
47
+ * File output uses `appendFileSync` rather than a `WriteStream` so each
48
+ * `debug()` line lands on disk before the function returns. That trades a
49
+ * little throughput for ordering guarantees that matter when debugging
50
+ * crashes — a streamed sink could lose its tail buffer on `process.exit`.
51
+ */
52
+ export declare function configureDebug(opts?: ConfigureDebugOptions): void;
53
+ /** Whether the given category is currently emitting. Cheap — a Set lookup. */
54
+ export declare function isDebugEnabled(category: string): boolean;
55
+ /**
56
+ * Emit a debug line for the given category. Cheap no-op when the category is
57
+ * disabled — argument formatting is skipped entirely. Each line is prefixed
58
+ * with `[debug:<cat>] +<elapsed_ms>ms` so categories interleave readably.
59
+ */
60
+ export declare function debug(category: string, ...args: unknown[]): void;
61
+ /** @internal Test-only: reset module-level state between cases. */
62
+ export declare function _resetDebugForTest(): void;
63
+ //# sourceMappingURL=debug.d.ts.map
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Categorized debug logger — gates verbose internal traces behind a runtime
3
+ * switch so they're silent by default but easy to flip on for support / CI.
4
+ *
5
+ * Activation precedence (highest first):
6
+ * 1. `configureDebug({ categories })` from a CLI flag (`--debug [cats]`)
7
+ * 2. `OH_DEBUG` env var
8
+ *
9
+ * Sink precedence:
10
+ * 1. `configureDebug({ file })` from `--debug-file <path>`
11
+ * 2. `OH_DEBUG_FILE` env var
12
+ * 3. `process.stderr` (default)
13
+ *
14
+ * Categories are arbitrary strings — call sites pick them. The CLI accepts a
15
+ * comma-separated list (`--debug mcp,hooks`) or `--debug` alone for "all".
16
+ *
17
+ * Wire pattern:
18
+ * import { configureDebug, debug } from "./utils/debug.js";
19
+ * configureDebug({ categories: opts.debug, file: opts.debugFile });
20
+ * debug("mcp", "connected", server.name);
21
+ */
22
+ import { appendFileSync } from "node:fs";
23
+ const ALL = "*";
24
+ let enabledCategories = new Set();
25
+ let debugFilePath;
26
+ let sinkOverride;
27
+ let started = Date.now();
28
+ /**
29
+ * Parse the raw flag value into a Set of enabled categories.
30
+ *
31
+ * Accepted values:
32
+ * - `undefined` / empty / `false` → no debug
33
+ * - `true` / `"*"` / `"all"` / `"1"` → all categories
34
+ * - `"mcp,hooks,provider"` → comma-separated explicit list
35
+ *
36
+ * Whitespace is trimmed and empty entries dropped, so `"mcp, ,hooks"` is
37
+ * equivalent to `"mcp,hooks"`. Pure function — exposed for testability.
38
+ */
39
+ export function parseDebugCategories(raw) {
40
+ if (raw === undefined || raw === false || raw === "")
41
+ return new Set();
42
+ if (raw === true)
43
+ return new Set([ALL]);
44
+ const lower = raw.toLowerCase();
45
+ if (lower === "*" || lower === "all" || lower === "true" || lower === "1")
46
+ return new Set([ALL]);
47
+ return new Set(raw
48
+ .split(",")
49
+ .map((s) => s.trim())
50
+ .filter(Boolean));
51
+ }
52
+ /**
53
+ * Apply debug configuration. Safe to call multiple times — later calls fully
54
+ * replace earlier state. When `categories` is undefined, falls back to
55
+ * `OH_DEBUG`; when `file` is undefined, falls back to `OH_DEBUG_FILE`.
56
+ *
57
+ * File output uses `appendFileSync` rather than a `WriteStream` so each
58
+ * `debug()` line lands on disk before the function returns. That trades a
59
+ * little throughput for ordering guarantees that matter when debugging
60
+ * crashes — a streamed sink could lose its tail buffer on `process.exit`.
61
+ */
62
+ export function configureDebug(opts = {}) {
63
+ const rawCats = opts.categories !== undefined ? opts.categories : process.env.OH_DEBUG;
64
+ enabledCategories = parseDebugCategories(rawCats);
65
+ sinkOverride = opts.sink;
66
+ debugFilePath = opts.sink ? undefined : (opts.file ?? process.env.OH_DEBUG_FILE);
67
+ started = Date.now();
68
+ }
69
+ /** Whether the given category is currently emitting. Cheap — a Set lookup. */
70
+ export function isDebugEnabled(category) {
71
+ return enabledCategories.has(ALL) || enabledCategories.has(category);
72
+ }
73
+ /**
74
+ * Emit a debug line for the given category. Cheap no-op when the category is
75
+ * disabled — argument formatting is skipped entirely. Each line is prefixed
76
+ * with `[debug:<cat>] +<elapsed_ms>ms` so categories interleave readably.
77
+ */
78
+ export function debug(category, ...args) {
79
+ if (!isDebugEnabled(category))
80
+ return;
81
+ const elapsed = Date.now() - started;
82
+ const formatted = args
83
+ .map((a) => {
84
+ if (typeof a === "string")
85
+ return a;
86
+ if (a instanceof Error)
87
+ return a.stack ?? a.message;
88
+ try {
89
+ return JSON.stringify(a);
90
+ }
91
+ catch {
92
+ return String(a);
93
+ }
94
+ })
95
+ .join(" ");
96
+ const line = `[debug:${category}] +${elapsed}ms ${formatted}\n`;
97
+ if (sinkOverride) {
98
+ sinkOverride.write(line);
99
+ }
100
+ else if (debugFilePath) {
101
+ try {
102
+ appendFileSync(debugFilePath, line);
103
+ }
104
+ catch (err) {
105
+ // Fall back to stderr so a broken --debug-file doesn't swallow output.
106
+ process.stderr.write(`[debug] could not append to '${debugFilePath}': ${err instanceof Error ? err.message : String(err)}\n`);
107
+ process.stderr.write(line);
108
+ debugFilePath = undefined;
109
+ }
110
+ }
111
+ else {
112
+ process.stderr.write(line);
113
+ }
114
+ }
115
+ /** @internal Test-only: reset module-level state between cases. */
116
+ export function _resetDebugForTest() {
117
+ enabledCategories = new Set();
118
+ debugFilePath = undefined;
119
+ sinkOverride = undefined;
120
+ started = Date.now();
121
+ }
122
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Detect how the running OH CLI was installed (audit B7) so `oh update` can
3
+ * print the appropriate upgrade command. Pure function — `detectInstallMethod`
4
+ * inspects the process's own filesystem path and current working directory;
5
+ * exported for unit testing and reuse.
6
+ *
7
+ * Detection rules, in order:
8
+ * - "local-clone" → `dist/main.js` lives inside a git repo whose root is
9
+ * the package itself (the user is running from a clone).
10
+ * Suggest `git pull && npm install && npm run build`.
11
+ * - "npm-global" → `dist/main.js` lives under a directory containing the
12
+ * segment `node_modules/@zhijiewang/openharness/`. This
13
+ * is the standard npm global install layout. Suggest
14
+ * `npm install -g @zhijiewang/openharness@latest`.
15
+ * - "npx-cache" → `dist/main.js` lives under a path containing
16
+ * `_npx/` (npx caches packages there). npx auto-fetches
17
+ * the latest by default; suggest re-running with
18
+ * `@latest` to bypass cache.
19
+ * - "unknown" → Couldn't classify. Print all three options and let
20
+ * the user choose.
21
+ */
22
+ export type InstallMethod = "local-clone" | "npm-global" | "npx-cache" | "unknown";
23
+ export interface InstallMethodResult {
24
+ method: InstallMethod;
25
+ /** The detected install root, mostly for diagnostics. */
26
+ root: string;
27
+ /** Multi-line user-facing message describing the upgrade command. */
28
+ message: string;
29
+ }
30
+ /**
31
+ * Classify the install method given the running script's filesystem path.
32
+ * `mainPath` defaults to `import.meta.url`-derived path in the CLI; tests
33
+ * override it.
34
+ */
35
+ export declare function detectInstallMethod(mainPath: string): InstallMethodResult;
36
+ /**
37
+ * Default `mainPath` resolver — walks up from `process.argv[1]` to find the
38
+ * package root. Exported so tests can stub it. Falls back to argv[1] verbatim
39
+ * when nothing matches.
40
+ */
41
+ export declare function getDefaultMainPath(): string;
42
+ //# sourceMappingURL=install-method.d.ts.map
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Detect how the running OH CLI was installed (audit B7) so `oh update` can
3
+ * print the appropriate upgrade command. Pure function — `detectInstallMethod`
4
+ * inspects the process's own filesystem path and current working directory;
5
+ * exported for unit testing and reuse.
6
+ *
7
+ * Detection rules, in order:
8
+ * - "local-clone" → `dist/main.js` lives inside a git repo whose root is
9
+ * the package itself (the user is running from a clone).
10
+ * Suggest `git pull && npm install && npm run build`.
11
+ * - "npm-global" → `dist/main.js` lives under a directory containing the
12
+ * segment `node_modules/@zhijiewang/openharness/`. This
13
+ * is the standard npm global install layout. Suggest
14
+ * `npm install -g @zhijiewang/openharness@latest`.
15
+ * - "npx-cache" → `dist/main.js` lives under a path containing
16
+ * `_npx/` (npx caches packages there). npx auto-fetches
17
+ * the latest by default; suggest re-running with
18
+ * `@latest` to bypass cache.
19
+ * - "unknown" → Couldn't classify. Print all three options and let
20
+ * the user choose.
21
+ */
22
+ import { existsSync } from "node:fs";
23
+ import { dirname, join, sep } from "node:path";
24
+ /**
25
+ * Classify the install method given the running script's filesystem path.
26
+ * `mainPath` defaults to `import.meta.url`-derived path in the CLI; tests
27
+ * override it.
28
+ */
29
+ export function detectInstallMethod(mainPath) {
30
+ // Normalize to forward slashes so the substring tests below work on Windows.
31
+ const normalized = mainPath.replace(/\\/g, "/");
32
+ // npx-cache: path contains `/_npx/` (Node's npx puts packages there)
33
+ if (normalized.includes("/_npx/")) {
34
+ return {
35
+ method: "npx-cache",
36
+ root: dirname(mainPath),
37
+ message: [
38
+ "You're running OH via npx (auto-fetched on each invocation).",
39
+ "To force the latest version on the next run, use:",
40
+ "",
41
+ " npx @zhijiewang/openharness@latest",
42
+ "",
43
+ "Or install globally to avoid the npx cache entirely:",
44
+ " npm install -g @zhijiewang/openharness@latest",
45
+ ].join("\n"),
46
+ };
47
+ }
48
+ // local-clone: walk up to find a package.json whose name matches AND a .git dir
49
+ let dir = dirname(mainPath);
50
+ while (dir && dir !== dirname(dir)) {
51
+ const pkgPath = join(dir, "package.json");
52
+ if (existsSync(pkgPath)) {
53
+ const isClone = existsSync(join(dir, ".git"));
54
+ if (isClone) {
55
+ return {
56
+ method: "local-clone",
57
+ root: dir,
58
+ message: [
59
+ `Detected a local clone at: ${dir}`,
60
+ "Pull the latest and rebuild:",
61
+ "",
62
+ ` cd ${dir}`,
63
+ " git pull && npm install && npm run build",
64
+ ].join("\n"),
65
+ };
66
+ }
67
+ // npm-global: the package.json belongs to OH and lives under a global
68
+ // node_modules directory.
69
+ if (normalized.includes("/node_modules/@zhijiewang/openharness/")) {
70
+ return {
71
+ method: "npm-global",
72
+ root: dir,
73
+ message: [
74
+ `Detected a global npm install at: ${dir}`,
75
+ "Upgrade with:",
76
+ "",
77
+ " npm install -g @zhijiewang/openharness@latest",
78
+ ].join("\n"),
79
+ };
80
+ }
81
+ break;
82
+ }
83
+ dir = dirname(dir);
84
+ }
85
+ return {
86
+ method: "unknown",
87
+ root: dirname(mainPath),
88
+ message: [
89
+ "Could not determine how OH was installed. Pick the option that matches your setup:",
90
+ "",
91
+ " Global npm install: npm install -g @zhijiewang/openharness@latest",
92
+ " npx (one-shot): npx @zhijiewang/openharness@latest",
93
+ " Local clone: git pull && npm install && npm run build",
94
+ ].join("\n"),
95
+ };
96
+ }
97
+ /**
98
+ * Default `mainPath` resolver — walks up from `process.argv[1]` to find the
99
+ * package root. Exported so tests can stub it. Falls back to argv[1] verbatim
100
+ * when nothing matches.
101
+ */
102
+ export function getDefaultMainPath() {
103
+ const entry = process.argv[1] ?? "";
104
+ if (!entry)
105
+ return "";
106
+ // If argv[1] points at a `dist/main.js`, that's already the right anchor.
107
+ // Otherwise return as-is and let `detectInstallMethod` figure it out.
108
+ return entry.split(sep).join("/");
109
+ }
110
+ //# sourceMappingURL=install-method.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.20.0",
3
+ "version": "2.22.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {