@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3

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 (179) hide show
  1. package/CHANGELOG.md +162 -40
  2. package/dist/cli.js +4559 -4543
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +39 -10
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/eval/agent-bridge.d.ts +7 -19
  13. package/dist/types/exec/bash-executor.d.ts +2 -0
  14. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  15. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  17. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
  18. package/dist/types/extensibility/shared-events.d.ts +6 -0
  19. package/dist/types/extensibility/utils.d.ts +2 -2
  20. package/dist/types/lsp/client.d.ts +2 -0
  21. package/dist/types/lsp/types.d.ts +3 -0
  22. package/dist/types/mcp/transports/stdio.d.ts +42 -1
  23. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  24. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  25. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  26. package/dist/types/modes/components/model-hub.d.ts +5 -2
  27. package/dist/types/modes/components/session-selector.d.ts +2 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  30. package/dist/types/modes/print-mode.d.ts +1 -1
  31. package/dist/types/modes/warp-events.d.ts +24 -0
  32. package/dist/types/modes/warp-events.test.d.ts +1 -0
  33. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  34. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  35. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  36. package/dist/types/registry/persisted-agents.d.ts +3 -0
  37. package/dist/types/sdk.d.ts +27 -5
  38. package/dist/types/session/agent-session.d.ts +34 -10
  39. package/dist/types/session/session-entries.d.ts +6 -1
  40. package/dist/types/session/session-history-format.d.ts +10 -0
  41. package/dist/types/session/session-manager.d.ts +19 -0
  42. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  43. package/dist/types/task/executor.d.ts +26 -1
  44. package/dist/types/task/index.d.ts +1 -1
  45. package/dist/types/task/label.d.ts +1 -1
  46. package/dist/types/task/parallel.d.ts +14 -0
  47. package/dist/types/task/structured-subagent.d.ts +111 -0
  48. package/dist/types/task/types.d.ts +51 -0
  49. package/dist/types/telemetry-export.d.ts +34 -9
  50. package/dist/types/tools/approval.d.ts +8 -0
  51. package/dist/types/tools/bash.d.ts +2 -0
  52. package/dist/types/tools/essential-tools.d.ts +29 -0
  53. package/dist/types/tools/fetch.d.ts +4 -5
  54. package/dist/types/tools/hub/messaging.d.ts +5 -1
  55. package/dist/types/tools/image-gen.d.ts +2 -1
  56. package/dist/types/tools/index.d.ts +17 -1
  57. package/dist/types/tools/todo.d.ts +31 -0
  58. package/dist/types/tools/xdev.d.ts +11 -2
  59. package/dist/types/tui/tree-list.d.ts +7 -0
  60. package/dist/types/utils/markit.d.ts +11 -0
  61. package/dist/types/utils/title-generator.d.ts +2 -1
  62. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  63. package/dist/types/web/search/types.d.ts +1 -1
  64. package/package.json +21 -16
  65. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  66. package/src/advisor/__tests__/config.test.ts +58 -2
  67. package/src/advisor/config.ts +76 -24
  68. package/src/advisor/runtime.ts +445 -92
  69. package/src/autolearn/controller.ts +23 -28
  70. package/src/cli/file-processor.ts +1 -2
  71. package/src/cli.ts +5 -1
  72. package/src/config/model-discovery.ts +81 -21
  73. package/src/config/model-registry.ts +25 -6
  74. package/src/config/model-resolver.ts +14 -7
  75. package/src/config/settings-schema.ts +46 -9
  76. package/src/config/settings.ts +405 -25
  77. package/src/cursor.ts +20 -3
  78. package/src/debug/report-bundle.ts +40 -4
  79. package/src/discovery/helpers.ts +28 -5
  80. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  81. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  82. package/src/eval/agent-bridge.ts +104 -477
  83. package/src/eval/jl/prelude.jl +7 -6
  84. package/src/eval/js/shared/prelude.txt +5 -4
  85. package/src/eval/py/prelude.py +11 -39
  86. package/src/eval/rb/prelude.rb +5 -6
  87. package/src/exec/bash-executor.ts +14 -5
  88. package/src/extensibility/custom-tools/loader.ts +3 -3
  89. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  90. package/src/extensibility/extensions/loader.ts +3 -3
  91. package/src/extensibility/extensions/managed-timers.ts +83 -0
  92. package/src/extensibility/extensions/runner.ts +26 -0
  93. package/src/extensibility/extensions/types.ts +18 -0
  94. package/src/extensibility/extensions/wrapper.ts +2 -1
  95. package/src/extensibility/hooks/loader.ts +3 -3
  96. package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
  97. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  98. package/src/extensibility/plugins/manager.ts +2 -2
  99. package/src/extensibility/shared-events.ts +6 -0
  100. package/src/extensibility/utils.ts +91 -25
  101. package/src/irc/bus.ts +22 -3
  102. package/src/launch/broker.ts +3 -2
  103. package/src/launch/client.ts +2 -2
  104. package/src/launch/presence.ts +2 -2
  105. package/src/lsp/client.ts +58 -1
  106. package/src/lsp/index.ts +62 -6
  107. package/src/lsp/types.ts +3 -0
  108. package/src/main.ts +11 -8
  109. package/src/mcp/manager.ts +9 -3
  110. package/src/mcp/oauth-flow.ts +20 -0
  111. package/src/mcp/transports/stdio.test.ts +269 -1
  112. package/src/mcp/transports/stdio.ts +255 -24
  113. package/src/modes/components/advisor-config.ts +65 -3
  114. package/src/modes/components/agent-hub.ts +1 -72
  115. package/src/modes/components/ask-dialog.ts +1 -1
  116. package/src/modes/components/bash-execution.ts +7 -3
  117. package/src/modes/components/eval-execution.ts +3 -1
  118. package/src/modes/components/hook-editor.ts +18 -3
  119. package/src/modes/components/model-hub.ts +138 -42
  120. package/src/modes/components/session-selector.ts +4 -0
  121. package/src/modes/components/status-line/component.test.ts +1 -0
  122. package/src/modes/components/status-line/segments.ts +21 -6
  123. package/src/modes/controllers/command-controller.ts +167 -47
  124. package/src/modes/controllers/event-controller.ts +5 -0
  125. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  126. package/src/modes/controllers/input-controller.ts +12 -12
  127. package/src/modes/controllers/selector-controller.ts +191 -31
  128. package/src/modes/interactive-mode.ts +169 -62
  129. package/src/modes/print-mode.ts +3 -3
  130. package/src/modes/rpc/host-tools.ts +2 -1
  131. package/src/modes/rpc/rpc-mode.ts +19 -4
  132. package/src/modes/warp-events.test.ts +794 -0
  133. package/src/modes/warp-events.ts +232 -0
  134. package/src/plan-mode/model-transition.test.ts +60 -0
  135. package/src/plan-mode/model-transition.ts +51 -0
  136. package/src/prompts/system/system-prompt.md +1 -1
  137. package/src/prompts/tools/eval.md +5 -2
  138. package/src/prompts/tools/read.md +1 -1
  139. package/src/prompts/tools/task.md +12 -8
  140. package/src/prompts/tools/write.md +1 -1
  141. package/src/registry/agent-lifecycle.ts +133 -18
  142. package/src/registry/persisted-agents.ts +74 -0
  143. package/src/sdk.ts +343 -122
  144. package/src/session/agent-session.ts +1359 -368
  145. package/src/session/session-entries.ts +6 -1
  146. package/src/session/session-history-format.ts +20 -5
  147. package/src/session/session-manager.ts +57 -0
  148. package/src/session/streaming-output.ts +41 -1
  149. package/src/slash-commands/builtin-registry.ts +7 -0
  150. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  151. package/src/system-prompt.ts +7 -2
  152. package/src/task/executor.ts +100 -22
  153. package/src/task/index.ts +258 -429
  154. package/src/task/label.ts +2 -0
  155. package/src/task/parallel.ts +43 -0
  156. package/src/task/persisted-revive.ts +19 -7
  157. package/src/task/structured-subagent.ts +642 -0
  158. package/src/task/types.ts +58 -0
  159. package/src/telemetry-export.ts +453 -97
  160. package/src/tools/__tests__/eval-description.test.ts +1 -1
  161. package/src/tools/approval.ts +11 -0
  162. package/src/tools/bash.ts +71 -38
  163. package/src/tools/essential-tools.ts +45 -0
  164. package/src/tools/fetch.ts +28 -105
  165. package/src/tools/gh.ts +169 -2
  166. package/src/tools/hub/messaging.ts +16 -3
  167. package/src/tools/image-gen.ts +69 -7
  168. package/src/tools/index.ts +50 -15
  169. package/src/tools/path-utils.ts +1 -0
  170. package/src/tools/read.ts +62 -29
  171. package/src/tools/todo.ts +126 -13
  172. package/src/tools/write.ts +22 -4
  173. package/src/tools/xdev.ts +14 -3
  174. package/src/tui/tree-list.ts +39 -0
  175. package/src/utils/markit.ts +12 -0
  176. package/src/utils/title-generator.ts +15 -4
  177. package/src/utils/zip.ts +16 -1
  178. package/src/web/search/providers/kimi.ts +18 -12
  179. package/src/web/search/types.ts +6 -1
@@ -741,13 +741,16 @@ export function buildExtensionModuleItems(
741
741
  * Entry for an installed Claude Code plugin.
742
742
  */
743
743
  export interface ClaudePluginEntry {
744
- scope: "user" | "project";
744
+ /** Claude registry scope; local entries are restricted to their project path. */
745
+ scope?: "user" | "project" | "local";
745
746
  installPath: string;
746
747
  version: string;
747
748
  installedAt: string;
748
749
  lastUpdated: string;
749
750
  gitCommitSha?: string;
750
751
  enabled?: boolean;
752
+ /** Project root recorded by Claude for a local installation. */
753
+ projectPath?: string;
751
754
  }
752
755
 
753
756
  /**
@@ -862,6 +865,14 @@ export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<
862
865
  return path.join(cwd, getConfigDirName(), "plugins", "installed_plugins.json");
863
866
  }
864
867
 
868
+ async function canonicalClaudeProjectPath(projectPath: string): Promise<string | null> {
869
+ try {
870
+ return await fs.promises.realpath(path.resolve(projectPath));
871
+ } catch {
872
+ return null;
873
+ }
874
+ }
875
+
865
876
  const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
866
877
 
867
878
  const pluginCacheInvalidators = new Set<() => void>();
@@ -876,20 +887,23 @@ export function registerPluginCacheInvalidator(invalidator: () => void): void {
876
887
  * Reads ~/.claude/plugins/installed_plugins.json and ~/.omp/plugins/installed_plugins.json,
877
888
  * and optionally the nearest project-scoped registry resolved from `cwd`.
878
889
  *
879
- * Results are cached per `home:resolvedProjectPath` key to avoid repeated parsing.
890
+ * Results are cached per home, project registry, and canonical active project.
880
891
  */
881
892
  export async function listClaudePluginRoots(
882
893
  home: string,
883
894
  cwd?: string,
884
895
  ): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
885
896
  const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
886
- const cacheKey = `${home}:${resolvedProjectPath ?? ""}`;
897
+ const projectRoot = resolvedProjectPath ? path.dirname(path.dirname(path.dirname(resolvedProjectPath))) : cwd;
898
+ const activeClaudeProjectPath = projectRoot ? await canonicalClaudeProjectPath(projectRoot) : null;
899
+ const cacheKey = `${home}:${resolvedProjectPath ?? ""}:${activeClaudeProjectPath ?? ""}`;
887
900
  const cached = pluginRootsCache.get(cacheKey);
888
901
  if (cached) return cached;
889
902
 
890
903
  const roots: ClaudePluginRoot[] = [];
891
904
  const warnings: string[] = [];
892
905
  const projectRoots: ClaudePluginRoot[] = [];
906
+ const canonicalClaudeProjectPaths = new Map<string, string | null>();
893
907
 
894
908
  // ── Claude Code registry ──────────────────────────────────────────────────
895
909
  const registryPath = path.join(home, ".claude", "plugins", "installed_plugins.json");
@@ -921,6 +935,15 @@ export async function listClaudePluginRoots(
921
935
  continue;
922
936
  }
923
937
  if (entry.enabled === false) continue;
938
+ if (entry.scope === "local") {
939
+ if (!entry.projectPath || !activeClaudeProjectPath) continue;
940
+ let entryProjectPath = canonicalClaudeProjectPaths.get(entry.projectPath);
941
+ if (entryProjectPath === undefined) {
942
+ entryProjectPath = await canonicalClaudeProjectPath(entry.projectPath);
943
+ canonicalClaudeProjectPaths.set(entry.projectPath, entryProjectPath);
944
+ }
945
+ if (entryProjectPath !== activeClaudeProjectPath) continue;
946
+ }
924
947
 
925
948
  roots.push({
926
949
  id: pluginId,
@@ -928,7 +951,7 @@ export async function listClaudePluginRoots(
928
951
  plugin: pluginName,
929
952
  version: entry.version || "unknown",
930
953
  path: entry.installPath,
931
- scope: entry.scope || "user",
954
+ scope: entry.scope === "local" ? "project" : entry.scope || "user",
932
955
  });
933
956
  }
934
957
  }
@@ -976,7 +999,7 @@ export async function listClaudePluginRoots(
976
999
  plugin: pluginName,
977
1000
  version: entry.version || "unknown",
978
1001
  path: entry.installPath,
979
- scope: entry.scope || "user",
1002
+ scope: entry.scope === "local" ? "project" : entry.scope || "user",
980
1003
  });
981
1004
  }
982
1005
  }
@@ -13,9 +13,9 @@ import type { ExecutorOptions } from "../../task/executor";
13
13
  import * as taskExecutor from "../../task/executor";
14
14
  import * as isolationRunner from "../../task/isolation-runner";
15
15
  import { AgentOutputManager } from "../../task/output-manager";
16
- import type { AgentDefinition, AgentProgress, SingleResult } from "../../task/types";
16
+ import type { AgentDefinition, AgentProgress, SingleResult, StructuredSubagentOutput } from "../../task/types";
17
17
  import type { ToolSession } from "../../tools";
18
- import { EVAL_AGENT_MAX_DEPTH, runEvalAgent } from "../agent-bridge";
18
+ import { runEvalAgent } from "../agent-bridge";
19
19
  import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP } from "../bridge-timeout";
20
20
  import { IdleTimeout } from "../idle-timeout";
21
21
  import { disposeAllVmContexts } from "../js/context-manager";
@@ -51,6 +51,7 @@ interface SessionOptions {
51
51
  settings?: Settings;
52
52
  outputManager?: AgentOutputManager;
53
53
  planMode?: boolean;
54
+ outputSchema?: unknown;
54
55
  }
55
56
 
56
57
  function makeSession(options: SessionOptions = {}): ToolSession {
@@ -76,6 +77,7 @@ function makeSession(options: SessionOptions = {}): ToolSession {
76
77
  getArtifactsDir: () => artifactsDir,
77
78
  getSessionId: () => "test-session",
78
79
  getEvalSessionId: () => "test-eval-session",
80
+ outputSchema: options.outputSchema,
79
81
  getPlanModeState: options.planMode
80
82
  ? () =>
81
83
  ({
@@ -189,7 +191,7 @@ describe("runEvalAgent", () => {
189
191
  );
190
192
  });
191
193
 
192
- it("enforces spawn restrictions and the eval recursion cap", async () => {
194
+ it("enforces shared spawn restrictions", async () => {
193
195
  mockAgents();
194
196
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
195
197
 
@@ -199,9 +201,6 @@ describe("runEvalAgent", () => {
199
201
  await expect(
200
202
  runEvalAgent({ prompt: "hello", agent: "task" }, { session: makeSession({ spawns: "reviewer" }) }),
201
203
  ).rejects.toThrow("Allowed: reviewer");
202
- await expect(
203
- runEvalAgent({ prompt: "hello" }, { session: makeSession({ depth: EVAL_AGENT_MAX_DEPTH }) }),
204
- ).rejects.toThrow("maximum depth");
205
204
  expect(runSpy).not.toHaveBeenCalled();
206
205
  });
207
206
 
@@ -219,12 +218,10 @@ describe("runEvalAgent", () => {
219
218
  expect(runSpy.mock.calls[0]?.[0].agent.name).toBe("reviewer");
220
219
  });
221
220
 
222
- it("honors task.maxRecursionDepth on top of the hard eval ceiling", async () => {
221
+ it("honors task.maxRecursionDepth without an eval-specific ceiling", async () => {
223
222
  mockAgents();
224
223
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
225
224
 
226
- // task.maxRecursionDepth=0 means "no spawning at all" — even depth 0 (the
227
- // top-level agent) must be blocked, matching canSpawnAtDepth().
228
225
  await expect(
229
226
  runEvalAgent(
230
227
  { prompt: "hello" },
@@ -240,35 +237,38 @@ describe("runEvalAgent", () => {
240
237
  ),
241
238
  ).rejects.toThrow("maximum depth is 0");
242
239
 
243
- // task.maxRecursionDepth=1 ("Single") lets the top spawn but a depth-1
244
- // subagent cannot spawn further — even though the hard ceiling is 3.
245
- await expect(
246
- runEvalAgent(
247
- { prompt: "hello" },
248
- {
249
- session: makeSession({
250
- depth: 1,
251
- settings: Settings.isolated({
252
- "async.enabled": false,
253
- "task.isolation.mode": "none",
254
- "task.maxRecursionDepth": 1,
255
- }),
240
+ await runEvalAgent(
241
+ { prompt: "hello" },
242
+ {
243
+ session: makeSession({
244
+ depth: 3,
245
+ settings: Settings.isolated({
246
+ "async.enabled": false,
247
+ "task.isolation.mode": "none",
248
+ "task.maxRecursionDepth": -1,
256
249
  }),
257
- },
258
- ),
259
- ).rejects.toThrow("maximum depth is 1");
260
-
261
- expect(runSpy).not.toHaveBeenCalled();
250
+ }),
251
+ },
252
+ );
253
+ expect(runSpy).toHaveBeenCalledTimes(1);
262
254
  });
263
255
 
264
- it("throws instead of spawning from plan mode", async () => {
265
- mockAgents();
256
+ it("runs plan-mode eval agents with an attenuated policy", async () => {
257
+ mockAgents([{ ...taskAgent, tools: ["ast_grep", "write"] }]);
266
258
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
267
259
 
268
- await expect(runEvalAgent({ prompt: "hello" }, { session: makeSession({ planMode: true }) })).rejects.toThrow(
269
- "unavailable in plan mode",
270
- );
271
- expect(runSpy).not.toHaveBeenCalled();
260
+ await expect(
261
+ runEvalAgent({ prompt: "hello" }, { session: makeSession({ planMode: true }) }),
262
+ ).resolves.toMatchObject({
263
+ text: "ok",
264
+ });
265
+ expect(runSpy).toHaveBeenCalledTimes(1);
266
+ expect(runSpy.mock.calls[0]?.[0].agent.tools).toEqual(["read", "grep", "glob", "web_search", "ast_grep"]);
267
+ expect(runSpy.mock.calls[0]?.[0].agent.spawns).toBeUndefined();
268
+ await expect(
269
+ runEvalAgent({ prompt: "unsafe", isolated: true }, { session: makeSession({ planMode: true }) }),
270
+ ).rejects.toThrow("isolation, apply, and merge controls are unavailable in plan mode");
271
+ expect(runSpy).toHaveBeenCalledTimes(1);
272
272
  });
273
273
 
274
274
  it("passes parent execution options and only sets outputSchema when schema is supplied", async () => {
@@ -310,8 +310,52 @@ describe("runEvalAgent", () => {
310
310
  expect(secondOptions.outputSchema).toBeUndefined();
311
311
  expect(secondOptions.outputSchemaOverridesAgent).toBeUndefined();
312
312
  });
313
+ it("returns host-parsed data for caller, agent, and inherited schemas", async () => {
314
+ const agentSchema = { type: "object" };
315
+ const sessionSchema = { type: "object" };
316
+ const callerSchema = { type: "object" };
317
+ const frontmatterAgent = { ...reviewerAgent, name: "structured", output: agentSchema };
318
+ mockAgents([taskAgent, frontmatterAgent]);
319
+ const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
320
+ const source = options.outputSchemaOverridesAgent
321
+ ? "caller"
322
+ : options.agent.name === "structured"
323
+ ? "agent"
324
+ : "session";
325
+ const structuredOutput: StructuredSubagentOutput = {
326
+ source,
327
+ mode: options.outputSchemaMode ?? "permissive",
328
+ status: "valid",
329
+ data: { source },
330
+ };
331
+ return singleResult(options, { output: "not JSON", structuredOutput });
332
+ });
333
+
334
+ const caller = await runEvalAgent(
335
+ { prompt: "caller", schema: callerSchema, schemaMode: "strict" },
336
+ { session: makeSession({ outputSchema: sessionSchema }) },
337
+ );
338
+ const frontmatter = await runEvalAgent(
339
+ { prompt: "agent", agent: "structured" },
340
+ { session: makeSession({ outputSchema: sessionSchema }) },
341
+ );
342
+ const inherited = await runEvalAgent(
343
+ { prompt: "session" },
344
+ { session: makeSession({ outputSchema: sessionSchema }) },
345
+ );
313
346
 
314
- it("forces LSP off for bridge subagents even when task.enableLsp is on", async () => {
347
+ expect(caller.data).toEqual({ source: "caller" });
348
+ expect(caller.details).toMatchObject({ schemaSource: "caller", schemaMode: "strict", schemaStatus: "valid" });
349
+ expect(frontmatter.data).toEqual({ source: "agent" });
350
+ expect(inherited.data).toEqual({ source: "session" });
351
+ expect(runSpy.mock.calls.map(([options]) => options.outputSchema)).toEqual([
352
+ callerSchema,
353
+ agentSchema,
354
+ sessionSchema,
355
+ ]);
356
+ });
357
+
358
+ it("inherits non-plan LSP and IRC policy for bridge subagents", async () => {
315
359
  mockAgents();
316
360
  const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
317
361
  // makeSession() defaults to enableLsp: true and task.enableLsp: true.
@@ -321,7 +365,8 @@ describe("runEvalAgent", () => {
321
365
 
322
366
  const options = runSpy.mock.calls[0]?.[0];
323
367
  if (!options) throw new Error("runSubprocess was not called");
324
- expect(options.enableLsp).toBe(false);
368
+ expect(options.enableLsp).toBe(true);
369
+ expect(options.enableIrc).toBe(true);
325
370
  expect(options.keepAlive).toBe(false);
326
371
  });
327
372
 
@@ -485,16 +530,30 @@ describe("agent() through eval runtimes", () => {
485
530
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options =>
486
531
  singleResult(options, {
487
532
  output: options.outputSchema ? '{"ok":true,"n":3}' : "hello from agent",
533
+ ...(options.outputSchema
534
+ ? {
535
+ structuredOutput: {
536
+ source: "caller",
537
+ mode: options.outputSchemaMode ?? "permissive",
538
+ status: "valid",
539
+ data: { ok: true, n: 3 },
540
+ } satisfies StructuredSubagentOutput,
541
+ }
542
+ : {}),
488
543
  }),
489
544
  );
490
545
 
491
546
  const result = await executeJs(
492
- 'const text = await agent("hi"); const data = await agent("json", { schema: { type: "object" } }); return JSON.stringify([text, data]);',
547
+ 'const text = await agent("hi"); const data = await agent("json", { schema: { type: "object" } }); const node = await agent("handle", { schema: { type: "object" }, handle: true }); return JSON.stringify({ text, data, node });',
493
548
  { cwd: tempDir.path(), sessionId: sharedJsSessionId, session, sessionFile },
494
549
  );
495
550
 
496
551
  expect(result.exitCode).toBe(0);
497
- expect(JSON.parse(result.output.trim())).toEqual(["hello from agent", { ok: true, n: 3 }]);
552
+ const output = JSON.parse(result.output.trim());
553
+ expect(output.text).toBe("hello from agent");
554
+ expect(output.data).toEqual({ ok: true, n: 3 });
555
+ expect(output.node.data).toEqual({ ok: true, n: 3 });
556
+ expect(output.node.handle).toBe(`agent://${output.node.id}`);
498
557
  });
499
558
 
500
559
  it("bounds JavaScript parallel() by the task.maxConcurrency setting while preserving order", async () => {
@@ -547,23 +606,43 @@ describe("agent() through eval runtimes", () => {
547
606
  const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "py-agent");
548
607
  mockAgents();
549
608
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options =>
550
- singleResult(options, { output: "hello from python" }),
609
+ singleResult(options, {
610
+ output: options.outputSchema ? "not JSON" : "hello from python",
611
+ ...(options.outputSchema
612
+ ? {
613
+ structuredOutput: {
614
+ source: "caller",
615
+ mode: options.outputSchemaMode ?? "permissive",
616
+ status: "valid",
617
+ data: { ok: true },
618
+ } satisfies StructuredSubagentOutput,
619
+ }
620
+ : {}),
621
+ }),
551
622
  );
552
623
 
553
- const result = await executePython('print(agent("hi"))', {
554
- cwd: tempDir.path(),
555
- sessionId,
556
- sessionFile,
557
- kernelMode: "per-call",
558
- toolSession: session,
559
- });
624
+ const result = await executePython(
625
+ 'import json\nprint(agent("hi"))\nprint(json.dumps(agent("structured", schema={"type": "object"})))\nnode = agent("handle", schema={"type": "object"}, handle=True)\nprint(json.dumps({"data": node["data"], "handle": node["handle"], "id": node["id"]}))',
626
+ {
627
+ cwd: tempDir.path(),
628
+ sessionId,
629
+ sessionFile,
630
+ kernelMode: "per-call",
631
+ toolSession: session,
632
+ },
633
+ );
560
634
  if (result.exitCode === undefined && result.cancelled) {
561
635
  expect(result.output).toBe("");
562
636
  return; // kernel unavailable in this environment
563
637
  }
564
638
 
565
639
  expect(result.exitCode).toBe(0);
566
- expect(result.output.trim()).toBe("hello from python");
640
+ const lines = result.output.trim().split("\n");
641
+ expect(lines[0]).toBe("hello from python");
642
+ expect(JSON.parse(lines[1] ?? "")).toEqual({ ok: true });
643
+ const node = JSON.parse(lines[2] ?? "");
644
+ expect(node.data).toEqual({ ok: true });
645
+ expect(node.handle).toBe(`agent://${node.id}`);
567
646
  });
568
647
 
569
648
  it("bounds Python parallel() by the task.maxConcurrency setting while preserving order", async () => {
@@ -772,7 +851,11 @@ describe("agent() through eval runtimes", () => {
772
851
 
773
852
  it("pauses the idle watchdog while a quiet agent() runs past the budget", async () => {
774
853
  using tempDir = TempDir.createSync("@omp-eval-agent-timeout-pause-");
775
- const { session } = makeEvalSession(tempDir, "js-agent-timeout-pause");
854
+ const { session } = makeEvalSession(
855
+ tempDir,
856
+ "js-agent-timeout-pause",
857
+ Settings.isolated({ "task.maxRuntimeMs": 1 }),
858
+ );
776
859
  mockAgents();
777
860
 
778
861
  // runSubprocess runs far past the eval timeout budget and emits NO progress
@@ -788,7 +871,9 @@ describe("agent() through eval runtimes", () => {
788
871
  const inFlight = new Promise<void>(resolve => {
789
872
  markInFlight = resolve;
790
873
  });
874
+ let observedMaxRuntimeMs: number | undefined;
791
875
  vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
876
+ observedMaxRuntimeMs = options.maxRuntimeMs;
792
877
  markInFlight?.();
793
878
  await released;
794
879
  return singleResult(options, { output: "done" });
@@ -812,6 +897,7 @@ describe("agent() through eval runtimes", () => {
812
897
 
813
898
  // The bridge paused the watchdog; the subprocess is now blocked in flight.
814
899
  await inFlight;
900
+ expect(observedMaxRuntimeMs).toBe(0);
815
901
  // Burn far more than the 20ms budget while paused: the watchdog stays armed-off.
816
902
  vi.advanceTimersByTime(1_000);
817
903
  expect(idle.signal.aborted).toBe(false);
@@ -53,6 +53,35 @@ describe("eval js agent() handle", () => {
53
53
  expect(out).toBe("hello world");
54
54
  });
55
55
 
56
+ it("keeps positional isolation controls stable while appending schemaMode", async () => {
57
+ let seenArgs: Record<string, unknown> | undefined;
58
+ const sandbox = loadPrelude(async (_name, args) => {
59
+ seenArgs = args as Record<string, unknown>;
60
+ return { text: '{"ok":true}', details: { agent: "task", id: "legacy", structured: false } };
61
+ });
62
+ const positionalAgent = sandbox.agent as (
63
+ prompt: string,
64
+ options?: unknown,
65
+ ...rest: unknown[]
66
+ ) => Promise<unknown>;
67
+ const schema = { type: "object", properties: { ok: { type: "boolean" } } };
68
+
69
+ await positionalAgent("scout", "reviewer", "p/model", "Legacy", schema, true, false, true, "strict");
70
+
71
+ expect(seenArgs).toEqual({
72
+ prompt: "scout",
73
+ agent: "reviewer",
74
+ model: "p/model",
75
+ label: "Legacy",
76
+ schema,
77
+ isolated: true,
78
+ apply: false,
79
+ merge: true,
80
+ schemaMode: "strict",
81
+ handle: false,
82
+ });
83
+ });
84
+
56
85
  it("carries the parsed object under data when schema and handle combine", async () => {
57
86
  const payload = JSON.stringify({ k: 1 });
58
87
  const sandbox = loadPrelude(async () => ({