@oh-my-pi/pi-coding-agent 14.1.1 → 14.2.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.
Files changed (123) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/package.json +8 -8
  3. package/scripts/build-binary.ts +61 -0
  4. package/src/autoresearch/helpers.ts +10 -0
  5. package/src/autoresearch/index.ts +1 -11
  6. package/src/autoresearch/tools/init-experiment.ts +1 -10
  7. package/src/autoresearch/tools/log-experiment.ts +1 -11
  8. package/src/autoresearch/tools/run-experiment.ts +1 -10
  9. package/src/bun-imports.d.ts +6 -0
  10. package/src/cli/plugin-cli.ts +23 -45
  11. package/src/commit/agentic/tools/propose-commit.ts +1 -14
  12. package/src/commit/agentic/tools/split-commit.ts +1 -15
  13. package/src/commit/utils.ts +15 -1
  14. package/src/config/model-registry.ts +3 -3
  15. package/src/config/prompt-templates.ts +4 -12
  16. package/src/config/settings-schema.ts +27 -2
  17. package/src/config/settings.ts +1 -1
  18. package/src/discovery/claude-plugins.ts +61 -6
  19. package/src/discovery/codex.ts +2 -15
  20. package/src/discovery/gemini.ts +2 -15
  21. package/src/discovery/helpers.ts +40 -1
  22. package/src/discovery/opencode.ts +2 -15
  23. package/src/edit/apply-patch/index.ts +87 -0
  24. package/src/edit/apply-patch/parser.ts +174 -0
  25. package/src/edit/diff.ts +3 -14
  26. package/src/edit/index.ts +65 -2
  27. package/src/edit/modes/apply-patch.lark +19 -0
  28. package/src/edit/modes/apply-patch.ts +63 -0
  29. package/src/edit/modes/hashline.ts +3 -3
  30. package/src/edit/modes/replace.ts +2 -13
  31. package/src/edit/read-file.ts +18 -0
  32. package/src/edit/renderer.ts +61 -33
  33. package/src/extensibility/extensions/compact-handler.ts +40 -0
  34. package/src/extensibility/extensions/runner.ts +11 -29
  35. package/src/extensibility/utils.ts +7 -1
  36. package/src/internal-urls/docs-index.generated.ts +9 -2
  37. package/src/lsp/render.ts +14 -2
  38. package/src/main.ts +1 -0
  39. package/src/mcp/manager.ts +29 -48
  40. package/src/memories/index.ts +7 -1
  41. package/src/modes/acp/acp-agent.ts +3 -16
  42. package/src/modes/components/model-selector.ts +15 -24
  43. package/src/modes/components/plugin-settings.ts +16 -5
  44. package/src/modes/components/read-tool-group.ts +92 -9
  45. package/src/modes/components/settings-defs.ts +18 -0
  46. package/src/modes/components/settings-selector.ts +2 -6
  47. package/src/modes/components/tool-execution.ts +61 -28
  48. package/src/modes/controllers/event-controller.ts +3 -1
  49. package/src/modes/controllers/extension-ui-controller.ts +99 -150
  50. package/src/modes/controllers/selector-controller.ts +3 -12
  51. package/src/modes/interactive-mode.ts +4 -2
  52. package/src/modes/print-mode.ts +4 -22
  53. package/src/modes/rpc/rpc-mode.ts +18 -38
  54. package/src/modes/shared.ts +10 -1
  55. package/src/modes/utils/ui-helpers.ts +6 -2
  56. package/src/plan-mode/approved-plan.ts +5 -4
  57. package/src/prompts/system/subagent-system-prompt.md +4 -4
  58. package/src/prompts/system/subagent-user-prompt.md +2 -2
  59. package/src/prompts/system/system-prompt.md +208 -243
  60. package/src/prompts/tools/apply-patch.md +67 -0
  61. package/src/prompts/tools/ast-edit.md +18 -23
  62. package/src/prompts/tools/ast-grep.md +24 -32
  63. package/src/prompts/tools/bash.md +11 -23
  64. package/src/prompts/tools/debug.md +8 -22
  65. package/src/prompts/tools/find.md +0 -4
  66. package/src/prompts/tools/grep.md +3 -5
  67. package/src/prompts/tools/hashline.md +16 -10
  68. package/src/prompts/tools/python.md +10 -14
  69. package/src/prompts/tools/read.md +17 -24
  70. package/src/prompts/tools/task.md +57 -21
  71. package/src/prompts/tools/todo-write.md +45 -67
  72. package/src/session/agent-session.ts +4 -4
  73. package/src/session/session-manager.ts +15 -7
  74. package/src/session/streaming-output.ts +24 -0
  75. package/src/slash-commands/builtin-registry.ts +3 -14
  76. package/src/task/executor.ts +13 -34
  77. package/src/task/index.ts +82 -18
  78. package/src/task/simple-mode.ts +27 -0
  79. package/src/task/template.ts +17 -3
  80. package/src/task/types.ts +77 -30
  81. package/src/tools/ask.ts +2 -4
  82. package/src/tools/ast-edit.ts +4 -15
  83. package/src/tools/ast-grep.ts +8 -27
  84. package/src/tools/bash-skill-urls.ts +9 -7
  85. package/src/tools/bash.ts +4 -12
  86. package/src/tools/browser.ts +1 -1
  87. package/src/tools/fetch.ts +1 -14
  88. package/src/tools/file-recorder.ts +35 -0
  89. package/src/tools/find.ts +6 -3
  90. package/src/tools/gh-format.ts +12 -0
  91. package/src/tools/gh-renderer.ts +1 -8
  92. package/src/tools/gh.ts +6 -13
  93. package/src/tools/grep.ts +9 -22
  94. package/src/tools/jtd-to-json-schema.ts +16 -0
  95. package/src/tools/match-line-format.ts +20 -0
  96. package/src/tools/path-utils.ts +30 -2
  97. package/src/tools/plan-mode-guard.ts +6 -5
  98. package/src/tools/python.ts +1 -1
  99. package/src/tools/read.ts +1 -1
  100. package/src/tools/render-utils.ts +38 -6
  101. package/src/tools/renderers.ts +1 -0
  102. package/src/tools/ssh.ts +3 -11
  103. package/src/tools/submit-result.ts +1 -13
  104. package/src/tools/todo-write.ts +137 -103
  105. package/src/tools/write.ts +2 -23
  106. package/src/tui/code-cell.ts +12 -7
  107. package/src/utils/edit-mode.ts +3 -2
  108. package/src/utils/git.ts +1 -1
  109. package/src/vim/engine.ts +41 -58
  110. package/src/web/scrapers/crates-io.ts +1 -14
  111. package/src/web/scrapers/types.ts +13 -0
  112. package/src/web/search/providers/base.ts +13 -0
  113. package/src/web/search/providers/brave.ts +2 -5
  114. package/src/web/search/providers/codex.ts +20 -24
  115. package/src/web/search/providers/gemini.ts +39 -1
  116. package/src/web/search/providers/jina.ts +2 -5
  117. package/src/web/search/providers/kagi.ts +3 -8
  118. package/src/web/search/providers/kimi.ts +3 -7
  119. package/src/web/search/providers/parallel.ts +3 -8
  120. package/src/web/search/providers/synthetic.ts +3 -7
  121. package/src/web/search/providers/tavily.ts +15 -11
  122. package/src/web/search/providers/utils.ts +36 -0
  123. package/src/web/search/providers/zai.ts +3 -7
package/src/task/index.ts CHANGED
@@ -18,6 +18,7 @@ import path from "node:path";
18
18
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
19
  import type { Usage } from "@oh-my-pi/pi-ai";
20
20
  import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
21
+ import type { TSchema } from "@sinclair/typebox";
21
22
  import type { ToolSession } from "..";
22
23
  import { resolveAgentModelPatterns } from "../config/model-resolver";
23
24
  import type { Theme } from "../modes/theme/theme";
@@ -34,17 +35,16 @@ import { runSubprocess } from "./executor";
34
35
  import { resolveIsolationBackendForTaskExecution } from "./isolation-backend";
35
36
  import { AgentOutputManager } from "./output-manager";
36
37
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
37
- import { renderCall, renderResult } from "./render";
38
+ import { renderResult, renderCall as renderTaskCall } from "./render";
39
+ import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
38
40
  import { renderTemplate } from "./template";
39
41
  import {
40
42
  type AgentDefinition,
41
43
  type AgentProgress,
44
+ getTaskSchema,
42
45
  type SingleResult,
43
46
  type TaskParams,
44
- type TaskSchema,
45
47
  type TaskToolDetails,
46
- taskSchema,
47
- taskSchemaNoIsolation,
48
48
  } from "./types";
49
49
  import {
50
50
  applyBaseline,
@@ -133,16 +133,54 @@ function renderDescription(
133
133
  isolationEnabled: boolean,
134
134
  asyncEnabled: boolean,
135
135
  disabledAgents: string[],
136
+ simpleMode: TaskSimpleMode,
136
137
  ): string {
137
138
  const filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
139
+ const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
138
140
  return prompt.render(taskDescriptionTemplate, {
139
141
  agents: filteredAgents,
140
142
  MAX_CONCURRENCY: maxConcurrency,
141
143
  isolationEnabled,
142
144
  asyncEnabled,
145
+ contextEnabled,
146
+ customSchemaEnabled,
147
+ defaultMode: simpleMode === "default",
148
+ schemaFreeMode: simpleMode === "schema-free",
149
+ independentMode: simpleMode === "independent",
143
150
  });
144
151
  }
145
152
 
153
+ function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
154
+ return {
155
+ content: [{ type: "text", text }],
156
+ details: { projectAgentsDir: null, results: [], totalDurationMs: 0 },
157
+ };
158
+ }
159
+
160
+ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams): string | undefined {
161
+ const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
162
+ const disallowedFields: string[] = [];
163
+ if (!contextEnabled && params.context !== undefined) {
164
+ disallowedFields.push("context");
165
+ }
166
+ if (!customSchemaEnabled && params.schema !== undefined) {
167
+ disallowedFields.push("schema");
168
+ }
169
+ if (disallowedFields.length === 0) {
170
+ return undefined;
171
+ }
172
+
173
+ if (simpleMode === "schema-free") {
174
+ return "task.simple is set to schema-free, so the task tool does not accept `schema`. Remove it and rely on the selected agent definition or inherited session schema.";
175
+ }
176
+
177
+ if (disallowedFields.length === 1) {
178
+ return `task.simple is set to independent, so the task tool does not accept \`${disallowedFields[0]}\`. Put everything the subagent needs inside each task assignment.`;
179
+ }
180
+
181
+ return "task.simple is set to independent, so the task tool does not accept `context` or `schema`. Put all required background and output expectations inside each task assignment or the selected agent definition.";
182
+ }
183
+
146
184
  // ═══════════════════════════════════════════════════════════════════════════
147
185
  // Tool Class
148
186
  // ═══════════════════════════════════════════════════════════════════════════
@@ -153,16 +191,23 @@ function renderDescription(
153
191
  * Requires async initialization to discover available agents.
154
192
  * Use `TaskTool.create(session)` to instantiate.
155
193
  */
156
- export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
194
+ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
157
195
  readonly name = "task";
158
196
  readonly label = "Task";
159
197
  readonly strict = true;
160
- readonly parameters: TaskSchema;
161
- readonly renderCall = renderCall;
162
198
  readonly renderResult = renderResult;
163
199
  readonly #discoveredAgents: AgentDefinition[];
164
200
  readonly #blockedAgent: string | undefined;
165
201
 
202
+ get parameters(): TSchema {
203
+ const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
204
+ return getTaskSchema({ isolationEnabled, simpleMode: this.#getTaskSimpleMode() });
205
+ }
206
+
207
+ renderCall(args: unknown, options: Parameters<typeof renderTaskCall>[1], theme: Theme) {
208
+ return renderTaskCall(args as TaskParams, options, theme);
209
+ }
210
+
166
211
  /** Dynamic description that reflects current disabled-agent settings */
167
212
  get description(): string {
168
213
  const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
@@ -174,33 +219,42 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
174
219
  isolationMode !== "none",
175
220
  this.session.settings.get("async.enabled"),
176
221
  disabledAgents,
222
+ this.#getTaskSimpleMode(),
177
223
  );
178
224
  }
179
225
  private constructor(
180
226
  private readonly session: ToolSession,
181
227
  discoveredAgents: AgentDefinition[],
182
- isolationEnabled: boolean,
183
228
  ) {
184
- this.parameters = isolationEnabled ? taskSchema : taskSchemaNoIsolation;
185
229
  this.#blockedAgent = $env.PI_BLOCKED_AGENT;
186
230
  this.#discoveredAgents = discoveredAgents;
187
231
  }
188
232
 
233
+ #getTaskSimpleMode(): TaskSimpleMode {
234
+ return this.session.settings.get("task.simple");
235
+ }
236
+
189
237
  /**
190
238
  * Create a TaskTool instance with async agent discovery.
191
239
  */
192
240
  static async create(session: ToolSession): Promise<TaskTool> {
193
- const isolationMode = session.settings.get("task.isolation.mode");
194
241
  const { agents } = await discoverAgents(session.cwd);
195
- return new TaskTool(session, agents, isolationMode !== "none");
242
+ return new TaskTool(session, agents);
196
243
  }
197
244
 
198
245
  async execute(
199
246
  _toolCallId: string,
200
- params: TaskParams,
247
+ rawParams: unknown,
201
248
  signal?: AbortSignal,
202
249
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
203
250
  ): Promise<AgentToolResult<TaskToolDetails>> {
251
+ const params = rawParams as TaskParams;
252
+ const simpleMode = this.#getTaskSimpleMode();
253
+ const validationError = validateTaskModeParams(simpleMode, params);
254
+ if (validationError) {
255
+ return createTaskModeError(validationError);
256
+ }
257
+
204
258
  const asyncEnabled = this.session.settings.get("async.enabled");
205
259
  const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
206
260
  if (!asyncEnabled || selectedAgent?.blocking === true) {
@@ -225,7 +279,9 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
225
279
  const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
226
280
  const fallbackAgentSource =
227
281
  this.#discoveredAgents.find(agent => agent.name === params.agent)?.source ?? "bundled";
228
- const renderedTasks = taskItems.map(taskItem => renderTemplate(params.context, taskItem));
282
+ const { contextEnabled } = getTaskSimpleModeCapabilities(simpleMode);
283
+ const sharedContext = contextEnabled ? params.context : undefined;
284
+ const renderedTasks = taskItems.map(taskItem => renderTemplate(sharedContext, taskItem, simpleMode));
229
285
  const progressByTaskId = new Map<string, AgentProgress>();
230
286
  for (let index = 0; index < renderedTasks.length; index++) {
231
287
  const renderedTask = renderedTasks[index];
@@ -445,6 +501,9 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
445
501
  const startTime = Date.now();
446
502
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
447
503
  const { agent: agentName, context, schema: outputSchema } = params;
504
+ const simpleMode = this.#getTaskSimpleMode();
505
+ const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
506
+ const sharedContext = contextEnabled ? context : undefined;
448
507
  const isolationMode = this.session.settings.get("task.isolation.mode");
449
508
  const isolationRequested = "isolated" in params ? params.isolated === true : false;
450
509
  const isIsolated = isolationMode !== "none" && isolationRequested;
@@ -530,8 +589,11 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
530
589
  });
531
590
  const thinkingLevelOverride = effectiveAgent.thinkingLevel;
532
591
 
533
- // Output schema priority: caller params > agent frontmatter > inherited from parent session
534
- const effectiveOutputSchema = outputSchema ?? effectiveAgent.output ?? this.session.outputSchema;
592
+ // Output schema priority: task call > agent frontmatter > inherited parent session.
593
+ // task.simple can disable the task-call override while leaving agent/session schemas intact.
594
+ const effectiveOutputSchema = customSchemaEnabled
595
+ ? (outputSchema ?? effectiveAgent.output ?? this.session.outputSchema)
596
+ : (effectiveAgent.output ?? this.session.outputSchema);
535
597
 
536
598
  // Handle empty or missing tasks
537
599
  if (!params.tasks || params.tasks.length === 0) {
@@ -539,7 +601,9 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
539
601
  content: [
540
602
  {
541
603
  type: "text",
542
- text: `No tasks provided. Use: { agent, context, tasks: [{id, description, args}, ...] }`,
604
+ text: contextEnabled
605
+ ? "No tasks provided. Use: { agent, context?, tasks: [{ id, description, assignment }, ...] }"
606
+ : "No tasks provided. Use: { agent, tasks: [{ id, description, assignment }, ...] }",
543
607
  },
544
608
  ],
545
609
  details: {
@@ -728,8 +792,8 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
728
792
  }
729
793
  const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
730
794
 
731
- // Build full prompts with context prepended
732
- const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(context, t));
795
+ // Build full prompts using shared context only when the current task mode allows it.
796
+ const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(sharedContext, t, simpleMode));
733
797
  const availableSkills = [...(this.session.skills ?? [])];
734
798
  const contextFiles = this.session.contextFiles?.filter(
735
799
  file => path.basename(file.path).toLowerCase() !== "agents.md",
@@ -0,0 +1,27 @@
1
+ export const TASK_SIMPLE_MODES = ["default", "schema-free", "independent"] as const;
2
+
3
+ export type TaskSimpleMode = (typeof TASK_SIMPLE_MODES)[number];
4
+
5
+ interface TaskSimpleModeCapabilities {
6
+ contextEnabled: boolean;
7
+ customSchemaEnabled: boolean;
8
+ }
9
+
10
+ const TASK_SIMPLE_MODE_CAPABILITIES: Record<TaskSimpleMode, TaskSimpleModeCapabilities> = {
11
+ default: {
12
+ contextEnabled: true,
13
+ customSchemaEnabled: true,
14
+ },
15
+ "schema-free": {
16
+ contextEnabled: true,
17
+ customSchemaEnabled: false,
18
+ },
19
+ independent: {
20
+ contextEnabled: false,
21
+ customSchemaEnabled: false,
22
+ },
23
+ };
24
+
25
+ export function getTaskSimpleModeCapabilities(mode: TaskSimpleMode): TaskSimpleModeCapabilities {
26
+ return TASK_SIMPLE_MODE_CAPABILITIES[mode];
27
+ }
@@ -1,5 +1,6 @@
1
1
  import { prompt } from "@oh-my-pi/pi-utils";
2
2
  import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
3
+ import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
3
4
  import type { TaskItem } from "./types";
4
5
 
5
6
  interface RenderResult {
@@ -16,16 +17,29 @@ interface RenderResult {
16
17
  *
17
18
  * If context is provided, it is prepended with a separator.
18
19
  */
19
- export function renderTemplate(context: string | undefined, task: TaskItem): RenderResult {
20
+ export function renderTemplate(
21
+ context: string | undefined,
22
+ task: TaskItem,
23
+ simpleMode: TaskSimpleMode = "default",
24
+ ): RenderResult {
20
25
  let { id, description, assignment } = task;
21
26
  assignment = assignment.trim();
22
- context = context?.trim();
27
+ const { contextEnabled } = getTaskSimpleModeCapabilities(simpleMode);
28
+ context = contextEnabled ? context?.trim() : undefined;
23
29
 
24
30
  if (!context || !assignment) {
31
+ if (simpleMode === "independent" && assignment) {
32
+ return {
33
+ task: prompt.render(subagentUserPromptTemplate, { assignment, independentMode: true }),
34
+ assignment,
35
+ id,
36
+ description,
37
+ };
38
+ }
25
39
  return { task: assignment || context!, assignment: assignment || context!, id, description };
26
40
  }
27
41
  return {
28
- task: prompt.render(subagentUserPromptTemplate, { context, assignment }),
42
+ task: prompt.render(subagentUserPromptTemplate, { context, assignment, independentMode: false }),
29
43
  assignment,
30
44
  id,
31
45
  description,
package/src/task/types.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Usage } from "@oh-my-pi/pi-ai";
3
3
  import { $env } from "@oh-my-pi/pi-utils";
4
- import { type Static, Type } from "@sinclair/typebox";
4
+ import { type Static, type TSchema, Type } from "@sinclair/typebox";
5
+ import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
5
6
  import type { NestedRepoPatch } from "./worktree";
6
7
 
7
8
  /** Source of an agent definition */
@@ -56,42 +57,58 @@ export interface SubagentLifecyclePayload {
56
57
  index: number;
57
58
  }
58
59
 
59
- /** Single task item for parallel execution */
60
- export const taskItemSchema = Type.Object({
61
- id: Type.String({
62
- description: "CamelCase identifier, max 48 chars",
63
- maxLength: 48,
64
- }),
65
- description: Type.String({
66
- description: "Short one-liner for UI display only — not seen by the subagent",
67
- }),
68
- assignment: Type.String({
69
- description:
70
- "Complete per-task instructions the subagent executes. Must follow the Target/Change/Edge Cases/Acceptance structure. Only include per-task deltas — shared background belongs in `context`.",
71
- }),
72
- });
60
+ const assignmentDescriptionForContextEnabled =
61
+ "Complete per-task instructions the subagent executes. Must follow the Target/Change/Edge Cases/Acceptance structure. Only include per-task deltas — shared background belongs in `context`.";
62
+ const assignmentDescriptionForContextDisabled =
63
+ "Complete per-task instructions the subagent executes. Must follow the Target/Change/Edge Cases/Acceptance structure, and include any background that would otherwise live in `context` since shared context is disabled in this mode.";
64
+
65
+ const createTaskItemSchema = (contextEnabled: boolean) =>
66
+ Type.Object({
67
+ id: Type.String({
68
+ description: "CamelCase identifier, max 48 chars",
69
+ maxLength: 48,
70
+ }),
71
+ description: Type.String({
72
+ description: "Short one-liner for UI display only — not seen by the subagent",
73
+ }),
74
+ assignment: Type.String({
75
+ description: contextEnabled ? assignmentDescriptionForContextEnabled : assignmentDescriptionForContextDisabled,
76
+ }),
77
+ });
78
+
79
+ /** Single task item for parallel execution (default shape with context enabled). */
80
+ export const taskItemSchema = createTaskItemSchema(true);
73
81
  export type TaskItem = Static<typeof taskItemSchema>;
74
82
 
75
- const createTaskSchema = (options: { isolationEnabled: boolean }) => {
76
- const properties = {
83
+ const createTaskSchema = (options: { isolationEnabled: boolean; simpleMode: TaskSimpleMode }) => {
84
+ const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(options.simpleMode);
85
+ const itemSchema = createTaskItemSchema(contextEnabled);
86
+ const properties: Record<string, TSchema> = {
77
87
  agent: Type.String({ description: "Agent type for all tasks in this batch" }),
78
- context: Type.Optional(
88
+ tasks: Type.Array(itemSchema, {
89
+ description: contextEnabled
90
+ ? "Tasks to execute in parallel. Each must be small-scoped (3-5 files max) and self-contained given context + assignment."
91
+ : "Tasks to execute in parallel. Each must be small-scoped (3-5 files max) and fully self-contained inside assignment because shared context is disabled.",
92
+ }),
93
+ };
94
+
95
+ if (contextEnabled) {
96
+ properties.context = Type.Optional(
79
97
  Type.String({
80
98
  description:
81
99
  "Shared background prepended to every task's assignment. Put goal, non-goals, constraints, conventions, reference paths, API contracts, and global acceptance commands here once — instead of duplicating across assignments.",
82
100
  }),
83
- ),
84
- schema: Type.Optional(
101
+ );
102
+ }
103
+
104
+ if (customSchemaEnabled) {
105
+ properties.schema = Type.Optional(
85
106
  Type.String({
86
107
  description:
87
108
  "JSON-encoded JTD schema defining expected response structure. Output format belongs here — never in context or assignment.",
88
109
  }),
89
- ),
90
- tasks: Type.Array(taskItemSchema, {
91
- description:
92
- "Tasks to execute in parallel. Each must be small-scoped (3-5 files max) and self-contained given context + assignment.",
93
- }),
94
- };
110
+ );
111
+ }
95
112
 
96
113
  if (options.isolationEnabled) {
97
114
  return Type.Object({
@@ -107,12 +124,42 @@ const createTaskSchema = (options: { isolationEnabled: boolean }) => {
107
124
  return Type.Object(properties);
108
125
  };
109
126
 
110
- export const taskSchema = createTaskSchema({ isolationEnabled: true });
111
- export const taskSchemaNoIsolation = createTaskSchema({ isolationEnabled: false });
127
+ export const taskSchema = createTaskSchema({ isolationEnabled: true, simpleMode: "default" });
128
+ export const taskSchemaNoIsolation = createTaskSchema({ isolationEnabled: false, simpleMode: "default" });
129
+ const taskSchemaSchemaFree = createTaskSchema({ isolationEnabled: true, simpleMode: "schema-free" });
130
+ const taskSchemaSchemaFreeNoIsolation = createTaskSchema({ isolationEnabled: false, simpleMode: "schema-free" });
131
+ const taskSchemaIndependent = createTaskSchema({ isolationEnabled: true, simpleMode: "independent" });
132
+ const taskSchemaIndependentNoIsolation = createTaskSchema({ isolationEnabled: false, simpleMode: "independent" });
133
+ const ALL_TASK_SCHEMAS = [
134
+ taskSchema,
135
+ taskSchemaNoIsolation,
136
+ taskSchemaSchemaFree,
137
+ taskSchemaSchemaFreeNoIsolation,
138
+ taskSchemaIndependent,
139
+ taskSchemaIndependentNoIsolation,
140
+ ] as const;
112
141
 
113
- export type TaskSchema = typeof taskSchema | typeof taskSchemaNoIsolation;
142
+ type DynamicTaskSchema = (typeof ALL_TASK_SCHEMAS)[number];
143
+ export type TaskSchema = typeof taskSchema;
114
144
 
115
- export type TaskParams = Static<TaskSchema>;
145
+ export function getTaskSchema(options: { isolationEnabled: boolean; simpleMode: TaskSimpleMode }): DynamicTaskSchema {
146
+ switch (options.simpleMode) {
147
+ case "schema-free":
148
+ return options.isolationEnabled ? taskSchemaSchemaFree : taskSchemaSchemaFreeNoIsolation;
149
+ case "independent":
150
+ return options.isolationEnabled ? taskSchemaIndependent : taskSchemaIndependentNoIsolation;
151
+ default:
152
+ return options.isolationEnabled ? taskSchema : taskSchemaNoIsolation;
153
+ }
154
+ }
155
+
156
+ export interface TaskParams {
157
+ agent: string;
158
+ context?: string;
159
+ schema?: string;
160
+ tasks: TaskItem[];
161
+ isolated?: boolean;
162
+ }
116
163
 
117
164
  /** A code review finding reported by the reviewer agent */
118
165
  export interface ReviewFinding {
package/src/tools/ask.ts CHANGED
@@ -407,10 +407,8 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
407
407
  ): Promise<AgentToolResult<AskToolDetails>> {
408
408
  // Headless fallback
409
409
  if (!context?.hasUI || !context.ui) {
410
- return {
411
- content: [{ type: "text" as const, text: "Error: User prompt requires interactive mode" }],
412
- details: {},
413
- };
410
+ context?.abort();
411
+ throw new ToolAbortError("Ask tool requires interactive mode");
414
412
  }
415
413
 
416
414
  const extensionUi = context.ui;
@@ -12,6 +12,7 @@ import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text
12
12
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
13
13
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
14
14
  import type { ToolSession } from ".";
15
+ import { createFileRecorder, formatResultPath } from "./file-recorder";
15
16
  import type { OutputMeta } from "./output-meta";
16
17
  import {
17
18
  combineSearchGlobs,
@@ -41,6 +42,7 @@ const astEditOpSchema = Type.Object({
41
42
 
42
43
  const astEditSchema = Type.Object({
43
44
  ops: Type.Array(astEditOpSchema, {
45
+ minItems: 1,
44
46
  description: "Rewrite ops as [{ pat, out }]",
45
47
  }),
46
48
  lang: Type.Optional(Type.String({ description: "Language override" })),
@@ -163,24 +165,11 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
163
165
  });
164
166
 
165
167
  const dedupedParseErrors = dedupeParseErrors(result.parseErrors);
166
- const formatPath = (filePath: string): string => {
167
- const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
168
- if (isDirectory) {
169
- return cleanPath.replace(/\\/g, "/");
170
- }
171
- return path.basename(cleanPath);
172
- };
168
+ const formatPath = (filePath: string): string => formatResultPath(filePath, isDirectory);
173
169
 
174
- const files = new Set<string>();
175
- const fileList: string[] = [];
170
+ const { record: recordFile, list: fileList } = createFileRecorder();
176
171
  const fileReplacementCounts = new Map<string, number>();
177
172
  const changesByFile = new Map<string, AstReplaceChange[]>();
178
- const recordFile = (relativePath: string) => {
179
- if (!files.has(relativePath)) {
180
- files.add(relativePath);
181
- fileList.push(relativePath);
182
- }
183
- };
184
173
  for (const fileChange of result.fileChanges) {
185
174
  const relativePath = formatPath(fileChange.path);
186
175
  recordFile(relativePath);
@@ -5,13 +5,14 @@ import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
- import { computeLineHash } from "../edit/line-hash";
9
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
9
  import type { Theme } from "../modes/theme/theme";
11
10
  import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
12
11
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
13
12
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
14
13
  import type { ToolSession } from ".";
14
+ import { createFileRecorder, formatResultPath } from "./file-recorder";
15
+ import { formatMatchLine } from "./match-line-format";
15
16
  import type { OutputMeta } from "./output-meta";
16
17
  import {
17
18
  combineSearchGlobs,
@@ -39,8 +40,8 @@ const astGrepSchema = Type.Object({
39
40
  path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to search (default: cwd)" })),
40
41
  glob: Type.Optional(Type.String({ description: "Optional glob filter relative to path" })),
41
42
  sel: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
42
- limit: Type.Optional(Type.Number({ description: "Max matches (default: 50)" })),
43
- offset: Type.Optional(Type.Number({ description: "Skip first N matches (default: 0)" })),
43
+ limit: Type.Optional(Type.Number({ description: "Max matches", default: 50 })),
44
+ offset: Type.Optional(Type.Number({ description: "Skip first N matches", default: 0 })),
44
45
  context: Type.Optional(Type.Number({ description: "Context lines around each match" })),
45
46
  });
46
47
 
@@ -155,24 +156,11 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
155
156
  return parseError?.[1] ?? error;
156
157
  });
157
158
  const dedupedParseErrors = dedupeParseErrors(normalizedParseErrors);
158
- const formatPath = (filePath: string): string => {
159
- const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
160
- if (isDirectory) {
161
- return cleanPath.replace(/\\/g, "/");
162
- }
163
- return path.basename(cleanPath);
164
- };
159
+ const formatPath = (filePath: string): string => formatResultPath(filePath, isDirectory);
165
160
 
166
- const files = new Set<string>();
167
- const fileList: string[] = [];
161
+ const { record: recordFile, list: fileList } = createFileRecorder();
168
162
  const fileMatchCounts = new Map<string, number>();
169
163
  const matchesByFile = new Map<string, AstFindMatch[]>();
170
- const recordFile = (relativePath: string) => {
171
- if (!files.has(relativePath)) {
172
- files.add(relativePath);
173
- fileList.push(relativePath);
174
- }
175
- };
176
164
  for (const match of result.matches) {
177
165
  const relativePath = formatPath(match.path);
178
166
  recordFile(relativePath);
@@ -211,15 +199,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
211
199
  const matchLines = match.text.split("\n");
212
200
  const lineNumbers = matchLines.map((_, index) => match.startLine + index);
213
201
  const lineWidth = Math.max(...lineNumbers.map(value => value.toString().length));
214
- const formatLine = (lineNumber: number, line: string, isMatch: boolean): string => {
215
- const separator = isMatch ? ":" : "-";
216
- if (useHashLines) {
217
- const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
218
- return `${ref}${separator}${line}`;
219
- }
220
- const padded = lineNumber.toString().padStart(lineWidth, " ");
221
- return `${padded}${separator}${line}`;
222
- };
202
+ const formatLine = (lineNumber: number, line: string, isMatch: boolean): string =>
203
+ formatMatchLine(lineNumber, line, isMatch, { useHashLines, lineWidth });
223
204
  for (let index = 0; index < matchLines.length; index++) {
224
205
  outputLines.push(formatLine(match.startLine + index, matchLines[index], index === 0));
225
206
  }
@@ -4,14 +4,14 @@ import type { Skill } from "../extensibility/skills";
4
4
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
5
5
  import { validateRelativePath } from "../internal-urls/skill-protocol";
6
6
  import type { InternalResource } from "../internal-urls/types";
7
+ import { normalizeLocalScheme } from "./path-utils";
7
8
  import { ToolError } from "./tool-errors";
8
9
 
9
10
  /** Regex to find skill:// tokens in command text. */
10
11
  const SKILL_URL_PATTERN = /'skill:\/\/[^'\s")`\\]+'|"skill:\/\/[^"\s')`\\]+"|skill:\/\/[^\s'")`\\]+/g;
11
12
 
12
- /** Regex to find supported internal URL tokens in command text. */
13
- const INTERNAL_URL_PATTERN =
14
- /'(?:skill|agent|artifact|plan|memory|rule|local):\/\/[^'\s")`\\]+'|"(?:skill|agent|artifact|plan|memory|rule|local):\/\/[^"\s')`\\]+"|(?:skill|agent|artifact|plan|memory|rule|local):\/\/[^\s'")`\\]+/g;
13
+ const INTERNAL_URL_PATTERN_INCLUDING_NORMALIZED_LOCAL =
14
+ /'(?:skill|agent|artifact|plan|memory|rule|local):\/\/[^'\s")`\\]+'|"(?:skill|agent|artifact|plan|memory|rule|local):\/\/[^"\s')`\\]+"|(?:skill|agent|artifact|plan|memory|rule|local):\/\/[^\s'")`\\]+|'local:\/[^'\s")`\\]+'|"local:\/[^"\s')`\\]+"|(?<![./\\\\\w-])local:\/[^\s'")`\\]+/g;
15
15
 
16
16
  const SUPPORTED_INTERNAL_SCHEMES = ["skill", "agent", "artifact", "plan", "memory", "rule", "local"] as const;
17
17
 
@@ -146,12 +146,13 @@ function shellEscape(p: string): string {
146
146
  }
147
147
 
148
148
  async function resolveInternalUrlToPath(
149
- url: string,
149
+ rawUrl: string,
150
150
  skills: readonly Skill[],
151
151
  internalRouter?: InternalUrlResolver,
152
152
  localOptions?: LocalProtocolOptions,
153
153
  ensureLocalParentDirs?: boolean,
154
154
  ): Promise<string> {
155
+ const url = normalizeLocalScheme(rawUrl);
155
156
  const scheme = extractScheme(url);
156
157
  if (!scheme) {
157
158
  throw new ToolError(`Unsupported internal URL in bash command: ${url}`);
@@ -218,9 +219,9 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
218
219
  * Supported schemes: skill://, agent://, artifact://, memory://, rule://, local://
219
220
  */
220
221
  export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
221
- if (!command.includes("://")) return command;
222
+ if (!command.includes("://") && !command.includes("local:/")) return command;
222
223
 
223
- const matches = Array.from(command.matchAll(INTERNAL_URL_PATTERN));
224
+ const matches = Array.from(command.matchAll(INTERNAL_URL_PATTERN_INCLUDING_NORMALIZED_LOCAL));
224
225
  if (matches.length === 0) return command;
225
226
 
226
227
  let expanded = command;
@@ -230,7 +231,8 @@ export async function expandInternalUrls(command: string, options: InternalUrlEx
230
231
  const index = match.index;
231
232
  if (index === undefined) continue;
232
233
 
233
- const url = unquoteToken(token);
234
+ const rawUrl = unquoteToken(token);
235
+ const url = normalizeLocalScheme(rawUrl);
234
236
  const resolvedPath = await resolveInternalUrlToPath(
235
237
  url,
236
238
  options.skills,
package/src/tools/bash.ts CHANGED
@@ -9,7 +9,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
12
- import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
12
+ import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
13
13
  import { renderStatusLine } from "../tui";
14
14
  import { CachedOutputBlock } from "../tui/output-block";
15
15
  import { getSixelLineMask } from "../utils/sixel";
@@ -38,7 +38,7 @@ const bashSchemaBase = Type.Object({
38
38
  "Additional environment variables passed to the command and rendered inline as shell assignments; prefer this for multiline or quote-heavy content",
39
39
  }),
40
40
  ),
41
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 300)" })),
41
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 300 })),
42
42
  cwd: Type.Optional(Type.String({ description: "Working directory (default: cwd)" })),
43
43
  head: Type.Optional(Type.Number({ description: "Return only first N lines of output" })),
44
44
  tail: Type.Optional(Type.Number({ description: "Return only last N lines of output" })),
@@ -512,7 +512,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
512
512
  : undefined;
513
513
 
514
514
  // Resolve protocol URLs (skill://, agent://, etc.) in extracted cwd.
515
- if (cwd?.includes("://")) {
515
+ if (cwd?.includes("://") || cwd?.includes("local:/")) {
516
516
  cwd = await expandInternalUrls(cwd, { ...internalUrlOptions, noEscape: true });
517
517
  }
518
518
 
@@ -612,15 +612,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
612
612
  env: resolvedEnv,
613
613
  artifactPath,
614
614
  artifactId,
615
- onChunk: chunk => {
616
- tailBuffer.append(chunk);
617
- if (onUpdate) {
618
- onUpdate({
619
- content: [{ type: "text", text: tailBuffer.text() }],
620
- details: {},
621
- });
622
- }
623
- },
615
+ onChunk: streamTailUpdates(tailBuffer, onUpdate),
624
616
  });
625
617
  if (result.cancelled) {
626
618
  if (signal?.aborted) {
@@ -411,7 +411,7 @@ const browserSchema = Type.Object({
411
411
  value: Type.Optional(Type.String({ description: "Value to set (fill)" })),
412
412
  attribute: Type.Optional(Type.String({ description: "Attribute name to read (get_attribute)" })),
413
413
  key: Type.Optional(Type.String({ description: "Keyboard key to press (press)" })),
414
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 30)" })),
414
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds", default: 30 })),
415
415
  wait_until: Type.Optional(
416
416
  StringEnum(["load", "domcontentloaded", "networkidle0", "networkidle2"], {
417
417
  description: "Navigation wait condition (goto)",