@zhijiewang/openharness 2.37.0 → 2.38.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.
@@ -8,7 +8,7 @@
8
8
  import type { Provider } from "../providers/base.js";
9
9
  import type { Tools } from "../Tool.js";
10
10
  import type { StreamEvent, ToolCallComplete, ToolCallEnd, ToolCallStart, ToolOutputDelta } from "../types/events.js";
11
- import type { PermissionMode } from "../types/permissions.js";
11
+ import { type PermissionMode } from "../types/permissions.js";
12
12
  /**
13
13
  * Forward inner-loop tool events to the outer stream, stamping parentCallId.
14
14
  * Exported for direct unit testing.
@@ -20,6 +20,15 @@ export type AgentTask = {
20
20
  description?: string;
21
21
  blockedBy?: string[];
22
22
  allowedTools?: string[];
23
+ /**
24
+ * Per-task permission mode override — narrowing-only, same contract as
25
+ * AgentTool's `permission_mode` (v2.36). When set, the task's effective
26
+ * mode is `clampSubagentPermissionMode(dispatcher.permissionMode, task.permissionMode)`,
27
+ * so a task can be the same strictness as the outer call or stricter,
28
+ * never looser. Use to mark specific tasks in a parallel batch as
29
+ * read-only review/audit while letting siblings keep full write access.
30
+ */
31
+ permissionMode?: PermissionMode;
23
32
  };
24
33
  export type AgentTaskResult = {
25
34
  id: string;
@@ -6,6 +6,7 @@
6
6
  * and triggers dependent tasks when their blockers complete.
7
7
  */
8
8
  import { createWorktree, isGitRepo, removeWorktree } from "../git/index.js";
9
+ import { clampSubagentPermissionMode } from "../types/permissions.js";
9
10
  /**
10
11
  * Forward inner-loop tool events to the outer stream, stamping parentCallId.
11
12
  * Exported for direct unit testing.
@@ -168,11 +169,15 @@ export class AgentDispatcher {
168
169
  // matching `process.chdir(originalCwd)` in `finally` — but since
169
170
  // `process.cwd()` is process-wide, two concurrent tasks would clobber
170
171
  // each other's directory mid-execution.
172
+ // Per-task permission mode — narrowing-only clamp applied so a task
173
+ // can override only to a same-or-stricter mode than the dispatcher's
174
+ // outer mode (#115 contract).
175
+ const taskPermissionMode = clampSubagentPermissionMode(this.permissionMode, task.permissionMode);
171
176
  const config = {
172
177
  provider: this.provider,
173
178
  tools: taskTools,
174
179
  systemPrompt: this.systemPrompt,
175
- permissionMode: this.permissionMode,
180
+ permissionMode: taskPermissionMode,
176
181
  model: this.model,
177
182
  maxTurns: 20,
178
183
  abortSignal: this.abortSignal,
@@ -6,15 +6,21 @@ declare const inputSchema: z.ZodObject<{
6
6
  prompt: z.ZodString;
7
7
  description: z.ZodOptional<z.ZodString>;
8
8
  blockedBy: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ allowed_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
10
+ permission_mode: z.ZodOptional<z.ZodEnum<["ask", "trust", "deny", "acceptEdits", "plan", "auto", "bypassPermissions"]>>;
9
11
  }, "strip", z.ZodTypeAny, {
10
12
  id: string;
11
13
  prompt: string;
12
14
  description?: string | undefined;
15
+ allowed_tools?: string[] | undefined;
16
+ permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
13
17
  blockedBy?: string[] | undefined;
14
18
  }, {
15
19
  id: string;
16
20
  prompt: string;
17
21
  description?: string | undefined;
22
+ allowed_tools?: string[] | undefined;
23
+ permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
18
24
  blockedBy?: string[] | undefined;
19
25
  }>, "many">;
20
26
  }, "strip", z.ZodTypeAny, {
@@ -22,6 +28,8 @@ declare const inputSchema: z.ZodObject<{
22
28
  id: string;
23
29
  prompt: string;
24
30
  description?: string | undefined;
31
+ allowed_tools?: string[] | undefined;
32
+ permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
25
33
  blockedBy?: string[] | undefined;
26
34
  }[];
27
35
  }, {
@@ -29,6 +37,8 @@ declare const inputSchema: z.ZodObject<{
29
37
  id: string;
30
38
  prompt: string;
31
39
  description?: string | undefined;
40
+ allowed_tools?: string[] | undefined;
41
+ permission_mode?: "ask" | "deny" | "trust" | "acceptEdits" | "plan" | "auto" | "bypassPermissions" | undefined;
32
42
  blockedBy?: string[] | undefined;
33
43
  }[];
34
44
  }>;
@@ -5,6 +5,11 @@ const taskSchema = z.object({
5
5
  prompt: z.string(),
6
6
  description: z.string().optional(),
7
7
  blockedBy: z.array(z.string()).optional(),
8
+ allowed_tools: z.array(z.string()).optional(),
9
+ permission_mode: z
10
+ .enum(["ask", "trust", "deny", "acceptEdits", "plan", "auto", "bypassPermissions"])
11
+ .optional()
12
+ .describe("Restrict THIS task's permission mode. Narrowing-only — clamps to the outer mode if a less-restrictive value is requested. Use to mark a single task as read-only review/audit while sibling tasks keep full write access."),
8
13
  });
9
14
  const inputSchema = z.object({
10
15
  tasks: z.array(taskSchema).min(1),
@@ -27,7 +32,18 @@ export const ParallelAgentTool = {
27
32
  const systemPrompt = context.systemPrompt ?? "You are a sub-agent. Complete the delegated task concisely.";
28
33
  const dispatcher = new AgentDispatcher(context.provider, context.tools, systemPrompt, context.permissionMode ?? "trust", context.model, context.workingDir, context.abortSignal, 4, // maxConcurrency default
29
34
  context.callId, context.emitChildEvent);
30
- dispatcher.addTasks(input.tasks);
35
+ // Map snake_case input fields to the AgentTask camelCase shape — the
36
+ // input schema uses `allowed_tools` / `permission_mode` to stay
37
+ // consistent with AgentTool, but the dispatcher's task type uses
38
+ // `allowedTools` / `permissionMode`.
39
+ dispatcher.addTasks(input.tasks.map((t) => ({
40
+ id: t.id,
41
+ prompt: t.prompt,
42
+ description: t.description,
43
+ blockedBy: t.blockedBy,
44
+ allowedTools: t.allowed_tools,
45
+ permissionMode: t.permission_mode,
46
+ })));
31
47
  const results = await dispatcher.execute();
32
48
  const output = results
33
49
  .map((r) => {
@@ -48,12 +64,13 @@ Parameters:
48
64
  - prompt (string): Instructions for the sub-agent
49
65
  - description (string, optional): Short label
50
66
  - blockedBy (string[], optional): IDs of tasks that must complete first
67
+ - allowed_tools (string[], optional): Restrict THIS task's agent to specific tools
68
+ - permission_mode (string, optional): Override THIS task's permission mode. Narrowing-only — a less-restrictive value clamps to the outer mode. Useful for marking review/audit tasks as "plan" or "deny" while sibling tasks keep full write access.
51
69
 
52
- Example: Run task A and B in parallel, then task C after both complete:
70
+ Example: parallel test-write + read-only review:
53
71
  tasks: [
54
- { id: "a", prompt: "..." },
55
- { id: "b", prompt: "..." },
56
- { id: "c", prompt: "...", blockedBy: ["a", "b"] }
72
+ { id: "tests", prompt: "Add tests for the new auth module" },
73
+ { id: "review", prompt: "Audit the new auth module for security issues", permission_mode: "plan" }
57
74
  ]`;
58
75
  },
59
76
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.37.0",
3
+ "version": "2.38.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {