@oh-my-pi/pi-coding-agent 13.2.0 → 13.3.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 (243) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -14
  4. package/scripts/generate-docs-index.ts +2 -2
  5. package/src/capability/index.ts +1 -2
  6. package/src/cli/args.ts +3 -3
  7. package/src/cli/config-cli.ts +1 -1
  8. package/src/cli/file-processor.ts +1 -2
  9. package/src/cli/grep-cli.ts +1 -1
  10. package/src/cli/jupyter-cli.ts +1 -1
  11. package/src/cli/plugin-cli.ts +1 -1
  12. package/src/cli/setup-cli.ts +1 -1
  13. package/src/cli/shell-cli.ts +1 -1
  14. package/src/cli/ssh-cli.ts +1 -1
  15. package/src/cli/stats-cli.ts +1 -2
  16. package/src/cli/update-cli.ts +1 -2
  17. package/src/cli/web-search-cli.ts +1 -1
  18. package/src/cli.ts +1 -1
  19. package/src/commands/launch.ts +2 -1
  20. package/src/commit/agentic/agent.ts +2 -1
  21. package/src/commit/agentic/index.ts +1 -2
  22. package/src/commit/agentic/prompts/system.md +3 -3
  23. package/src/commit/agentic/tools/propose-changelog.ts +30 -19
  24. package/src/commit/changelog/generate.ts +16 -6
  25. package/src/commit/changelog/index.ts +2 -1
  26. package/src/commit/pipeline.ts +1 -2
  27. package/src/commit/prompts/reduce-system.md +1 -1
  28. package/src/commit/types.ts +10 -1
  29. package/src/config/keybindings.ts +1 -2
  30. package/src/config/model-registry.ts +1 -1
  31. package/src/config/prompt-templates.ts +14 -2
  32. package/src/config/settings-schema.ts +36 -4
  33. package/src/config/settings.ts +19 -2
  34. package/src/config.ts +1 -2
  35. package/src/debug/index.ts +1 -1
  36. package/src/debug/report-bundle.ts +1 -2
  37. package/src/debug/system-info.ts +1 -2
  38. package/src/discovery/agents.ts +2 -2
  39. package/src/discovery/builtin.ts +8 -9
  40. package/src/discovery/claude-plugins.ts +2 -2
  41. package/src/discovery/claude.ts +30 -12
  42. package/src/discovery/codex.ts +3 -3
  43. package/src/discovery/cursor.ts +5 -4
  44. package/src/discovery/gemini.ts +5 -5
  45. package/src/discovery/helpers.ts +47 -69
  46. package/src/discovery/mcp-json.ts +3 -3
  47. package/src/discovery/opencode.ts +7 -8
  48. package/src/discovery/ssh.ts +3 -3
  49. package/src/discovery/vscode.ts +3 -2
  50. package/src/discovery/windsurf.ts +3 -2
  51. package/src/exa/company.ts +1 -1
  52. package/src/exa/factory.ts +1 -6
  53. package/src/exa/linkedin.ts +1 -1
  54. package/src/exa/mcp-client.ts +19 -8
  55. package/src/exa/search.ts +2 -2
  56. package/src/exa/types.ts +3 -3
  57. package/src/exec/bash-executor.ts +2 -1
  58. package/src/exec/non-interactive-env.ts +43 -0
  59. package/src/export/custom-share.ts +1 -1
  60. package/src/export/html/index.ts +1 -2
  61. package/src/extensibility/custom-commands/loader.ts +1 -2
  62. package/src/extensibility/plugins/installer.ts +1 -2
  63. package/src/extensibility/plugins/loader.ts +1 -2
  64. package/src/extensibility/plugins/manager.ts +3 -2
  65. package/src/extensibility/skills.ts +59 -115
  66. package/src/index.ts +1 -3
  67. package/src/internal-urls/docs-index.generated.ts +1 -1
  68. package/src/ipy/executor.ts +1 -2
  69. package/src/ipy/gateway-coordinator.ts +1 -2
  70. package/src/ipy/modules.ts +1 -1
  71. package/src/ipy/runtime.ts +2 -3
  72. package/src/main.ts +1 -2
  73. package/src/mcp/config.ts +2 -2
  74. package/src/mcp/transports/stdio.ts +1 -2
  75. package/src/memories/index.ts +1 -2
  76. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  77. package/src/modes/components/extensions/inspector-panel.ts +8 -2
  78. package/src/modes/components/footer.ts +1 -2
  79. package/src/modes/components/settings-defs.ts +17 -1
  80. package/src/modes/components/status-line/segments.ts +1 -2
  81. package/src/modes/components/status-line.ts +7 -5
  82. package/src/modes/components/tool-execution.ts +3 -10
  83. package/src/modes/components/welcome.ts +1 -1
  84. package/src/modes/controllers/command-controller.ts +1 -2
  85. package/src/modes/controllers/mcp-command-controller.ts +5 -4
  86. package/src/modes/controllers/selector-controller.ts +22 -1
  87. package/src/modes/controllers/ssh-command-controller.ts +1 -1
  88. package/src/modes/interactive-mode.ts +11 -3
  89. package/src/modes/oauth-manual-input.ts +42 -0
  90. package/src/modes/shared.ts +1 -2
  91. package/src/modes/theme/theme.ts +1 -2
  92. package/src/modes/types.ts +2 -0
  93. package/src/patch/hashline.ts +19 -1
  94. package/src/patch/index.ts +1 -25
  95. package/src/prompts/agents/designer.md +7 -10
  96. package/src/prompts/agents/explore.md +15 -23
  97. package/src/prompts/agents/init.md +23 -23
  98. package/src/prompts/agents/plan.md +14 -77
  99. package/src/prompts/agents/reviewer.md +6 -5
  100. package/src/prompts/agents/task.md +13 -11
  101. package/src/prompts/compaction/branch-summary.md +3 -3
  102. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  103. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  104. package/src/prompts/compaction/compaction-summary.md +5 -5
  105. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  106. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  107. package/src/prompts/memories/consolidation.md +5 -5
  108. package/src/prompts/memories/read-path.md +6 -6
  109. package/src/prompts/memories/stage_one_input.md +1 -1
  110. package/src/prompts/memories/stage_one_system.md +5 -5
  111. package/src/prompts/review-request.md +4 -4
  112. package/src/prompts/system/agent-creation-architect.md +17 -17
  113. package/src/prompts/system/agent-creation-user.md +2 -2
  114. package/src/prompts/system/commit-message-system.md +2 -0
  115. package/src/prompts/system/custom-system-prompt.md +4 -4
  116. package/src/prompts/system/plan-mode-active.md +20 -20
  117. package/src/prompts/system/plan-mode-approved.md +7 -7
  118. package/src/prompts/system/plan-mode-reference.md +2 -2
  119. package/src/prompts/system/plan-mode-subagent.md +8 -8
  120. package/src/prompts/system/subagent-submit-reminder.md +5 -5
  121. package/src/prompts/system/subagent-system-prompt.md +29 -22
  122. package/src/prompts/system/subagent-user-prompt.md +7 -3
  123. package/src/prompts/system/summarization-system.md +1 -1
  124. package/src/prompts/system/system-prompt.md +214 -226
  125. package/src/prompts/system/title-system.md +2 -2
  126. package/src/prompts/system/ttsr-interrupt.md +1 -1
  127. package/src/prompts/system/web-search.md +16 -16
  128. package/src/prompts/tools/ask.md +1 -3
  129. package/src/prompts/tools/await.md +2 -4
  130. package/src/prompts/tools/bash.md +5 -7
  131. package/src/prompts/tools/browser.md +4 -6
  132. package/src/prompts/tools/calculator.md +1 -3
  133. package/src/prompts/tools/cancel-job.md +2 -4
  134. package/src/prompts/tools/exit-plan-mode.md +7 -7
  135. package/src/prompts/tools/fetch.md +0 -2
  136. package/src/prompts/tools/find.md +3 -5
  137. package/src/prompts/tools/gemini-image.md +6 -22
  138. package/src/prompts/tools/grep.md +4 -6
  139. package/src/prompts/tools/hashline.md +56 -15
  140. package/src/prompts/tools/lsp.md +1 -3
  141. package/src/prompts/tools/patch.md +7 -9
  142. package/src/prompts/tools/python.md +10 -14
  143. package/src/prompts/tools/read.md +0 -2
  144. package/src/prompts/tools/replace.md +5 -7
  145. package/src/prompts/tools/ssh.md +3 -5
  146. package/src/prompts/tools/task-summary.md +4 -4
  147. package/src/prompts/tools/task.md +7 -9
  148. package/src/prompts/tools/todo-write.md +7 -9
  149. package/src/prompts/tools/web-search.md +3 -5
  150. package/src/prompts/tools/write.md +3 -5
  151. package/src/sdk.ts +4 -2
  152. package/src/session/agent-session.ts +10 -26
  153. package/src/session/agent-storage.ts +1 -2
  154. package/src/session/history-storage.ts +1 -2
  155. package/src/session/session-manager.ts +10 -2
  156. package/src/slash-commands/builtin-registry.ts +26 -1
  157. package/src/ssh/connection-manager.ts +11 -2
  158. package/src/ssh/sshfs-mount.ts +7 -1
  159. package/src/system-prompt.ts +29 -103
  160. package/src/task/agents.ts +1 -1
  161. package/src/task/index.ts +211 -70
  162. package/src/task/render.ts +24 -8
  163. package/src/task/types.ts +6 -1
  164. package/src/task/worktree.ts +394 -32
  165. package/src/tools/ask.ts +0 -1
  166. package/src/tools/bash-interactive.ts +2 -45
  167. package/src/tools/bash.ts +5 -5
  168. package/src/tools/browser.ts +1 -2
  169. package/src/tools/gemini-image.ts +8 -28
  170. package/src/tools/json-tree.ts +2 -1
  171. package/src/tools/python.ts +1 -1
  172. package/src/tools/read.ts +1 -2
  173. package/src/tools/submit-result.ts +22 -23
  174. package/src/utils/commit-message-generator.ts +132 -0
  175. package/src/utils/tools-manager.ts +1 -2
  176. package/src/web/scrapers/artifacthub.ts +2 -1
  177. package/src/web/scrapers/aur.ts +2 -1
  178. package/src/web/scrapers/biorxiv.ts +2 -1
  179. package/src/web/scrapers/bluesky.ts +2 -1
  180. package/src/web/scrapers/chocolatey.ts +2 -1
  181. package/src/web/scrapers/cisa-kev.ts +2 -1
  182. package/src/web/scrapers/clojars.ts +2 -1
  183. package/src/web/scrapers/coingecko.ts +2 -1
  184. package/src/web/scrapers/crates-io.ts +2 -1
  185. package/src/web/scrapers/crossref.ts +2 -1
  186. package/src/web/scrapers/discogs.ts +3 -1
  187. package/src/web/scrapers/discourse.ts +2 -1
  188. package/src/web/scrapers/dockerhub.ts +2 -1
  189. package/src/web/scrapers/fdroid.ts +2 -1
  190. package/src/web/scrapers/firefox-addons.ts +2 -1
  191. package/src/web/scrapers/flathub.ts +2 -1
  192. package/src/web/scrapers/gitlab.ts +1 -1
  193. package/src/web/scrapers/go-pkg.ts +2 -1
  194. package/src/web/scrapers/hackage.ts +2 -1
  195. package/src/web/scrapers/hackernews.ts +2 -1
  196. package/src/web/scrapers/hex.ts +2 -1
  197. package/src/web/scrapers/huggingface.ts +2 -1
  198. package/src/web/scrapers/jetbrains-marketplace.ts +2 -1
  199. package/src/web/scrapers/lemmy.ts +2 -1
  200. package/src/web/scrapers/lobsters.ts +2 -1
  201. package/src/web/scrapers/mastodon.ts +2 -1
  202. package/src/web/scrapers/maven.ts +2 -1
  203. package/src/web/scrapers/mdn.ts +2 -1
  204. package/src/web/scrapers/metacpan.ts +2 -1
  205. package/src/web/scrapers/musicbrainz.ts +3 -1
  206. package/src/web/scrapers/npm.ts +2 -1
  207. package/src/web/scrapers/nuget.ts +2 -1
  208. package/src/web/scrapers/nvd.ts +2 -1
  209. package/src/web/scrapers/ollama.ts +2 -1
  210. package/src/web/scrapers/open-vsx.ts +2 -1
  211. package/src/web/scrapers/opencorporates.ts +2 -1
  212. package/src/web/scrapers/openlibrary.ts +2 -1
  213. package/src/web/scrapers/orcid.ts +3 -1
  214. package/src/web/scrapers/osv.ts +2 -1
  215. package/src/web/scrapers/packagist.ts +2 -1
  216. package/src/web/scrapers/pub-dev.ts +2 -1
  217. package/src/web/scrapers/pubmed.ts +2 -1
  218. package/src/web/scrapers/pypi.ts +2 -1
  219. package/src/web/scrapers/rawg.ts +2 -8
  220. package/src/web/scrapers/reddit.ts +2 -1
  221. package/src/web/scrapers/repology.ts +2 -1
  222. package/src/web/scrapers/rfc.ts +2 -1
  223. package/src/web/scrapers/rubygems.ts +2 -1
  224. package/src/web/scrapers/searchcode.ts +2 -1
  225. package/src/web/scrapers/sec-edgar.ts +2 -1
  226. package/src/web/scrapers/semantic-scholar.ts +2 -1
  227. package/src/web/scrapers/snapcraft.ts +2 -1
  228. package/src/web/scrapers/sourcegraph.ts +2 -1
  229. package/src/web/scrapers/spdx.ts +2 -1
  230. package/src/web/scrapers/stackoverflow.ts +2 -1
  231. package/src/web/scrapers/terraform.ts +2 -1
  232. package/src/web/scrapers/types.ts +0 -11
  233. package/src/web/scrapers/vimeo.ts +2 -1
  234. package/src/web/scrapers/vscode-marketplace.ts +2 -1
  235. package/src/web/scrapers/w3c.ts +2 -1
  236. package/src/web/scrapers/wikidata.ts +2 -1
  237. package/src/web/search/index.ts +10 -14
  238. package/src/web/search/provider.ts +2 -2
  239. package/src/web/search/providers/codex.ts +1 -2
  240. package/src/web/search/providers/exa.ts +42 -10
  241. package/src/web/search/providers/gemini.ts +1 -1
  242. package/src/web/search/providers/perplexity.ts +20 -9
  243. package/src/web/search/providers/utils.ts +1 -1
package/src/task/index.ts CHANGED
@@ -29,6 +29,7 @@ import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type:
29
29
  import { formatBytes, formatDuration } from "../tools/render-utils";
30
30
  // Import review tools for side effects (registers subagent tool handlers)
31
31
  import "../tools/review";
32
+ import { generateCommitMessage } from "../utils/commit-message-generator";
32
33
  import { discoverAgents, getAgent } from "./discovery";
33
34
  import { runSubprocess } from "./executor";
34
35
  import { AgentOutputManager } from "./output-manager";
@@ -47,11 +48,17 @@ import {
47
48
  } from "./types";
48
49
  import {
49
50
  applyBaseline,
51
+ applyNestedPatches,
50
52
  captureBaseline,
51
53
  captureDeltaPatch,
54
+ cleanupFuseOverlay,
55
+ cleanupTaskBranches,
52
56
  cleanupWorktree,
57
+ commitToBranch,
58
+ ensureFuseOverlay,
53
59
  ensureWorktree,
54
60
  getRepoRoot,
61
+ mergeTaskBranches,
55
62
  type WorktreeBaseline,
56
63
  } from "./worktree";
57
64
 
@@ -145,11 +152,11 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
145
152
  get description(): string {
146
153
  const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
147
154
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
148
- const isolationEnabled = this.session.settings.get("task.isolation.enabled");
155
+ const isolationMode = this.session.settings.get("task.isolation.mode");
149
156
  return renderDescription(
150
157
  this.#discoveredAgents,
151
158
  maxConcurrency,
152
- isolationEnabled,
159
+ isolationMode !== "none",
153
160
  this.session.settings.get("async.enabled"),
154
161
  disabledAgents,
155
162
  );
@@ -168,9 +175,9 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
168
175
  * Create a TaskTool instance with async agent discovery.
169
176
  */
170
177
  static async create(session: ToolSession): Promise<TaskTool> {
171
- const isolationEnabled = session.settings.get("task.isolation.enabled");
178
+ const isolationMode = session.settings.get("task.isolation.mode");
172
179
  const { agents } = await discoverAgents(session.cwd);
173
- return new TaskTool(session, agents, isolationEnabled);
180
+ return new TaskTool(session, agents, isolationMode !== "none");
174
181
  }
175
182
 
176
183
  async execute(
@@ -422,18 +429,20 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
422
429
  const startTime = Date.now();
423
430
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
424
431
  const { agent: agentName, context, schema: outputSchema } = params;
425
- const isolationEnabled = this.session.settings.get("task.isolation.enabled");
432
+ const isolationMode = this.session.settings.get("task.isolation.mode");
426
433
  const isolationRequested = "isolated" in params ? params.isolated === true : false;
427
- const isIsolated = isolationEnabled && isolationRequested;
434
+ const isIsolated = isolationMode !== "none" && isolationRequested;
435
+ const mergeMode = this.session.settings.get("task.isolation.merge");
436
+ const commitStyle = this.session.settings.get("task.isolation.commits");
428
437
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
429
438
  const taskDepth = this.session.taskDepth ?? 0;
430
439
 
431
- if (!isolationEnabled && "isolated" in params) {
440
+ if (isolationMode === "none" && "isolated" in params) {
432
441
  return {
433
442
  content: [
434
443
  {
435
444
  type: "text",
436
- text: "Task isolation is disabled. Remove the isolated argument to run subagents.",
445
+ text: "Task isolation is disabled. Remove the isolated argument or set task.isolation.mode to 'worktree' or 'fuse-overlay'.",
437
446
  },
438
447
  ],
439
448
  details: {
@@ -789,16 +798,23 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
789
798
  }
790
799
 
791
800
  const taskStart = Date.now();
792
- let worktreeDir: string | undefined;
801
+ let isolationDir: string | undefined;
793
802
  try {
794
803
  if (!repoRoot || !baseline) {
795
804
  throw new Error("Isolated task execution not initialized.");
796
805
  }
797
- worktreeDir = await ensureWorktree(repoRoot, task.id);
798
- await applyBaseline(worktreeDir, baseline);
806
+ const taskBaseline = structuredClone(baseline);
807
+
808
+ if (isolationMode === "fuse-overlay") {
809
+ isolationDir = await ensureFuseOverlay(repoRoot, task.id);
810
+ } else {
811
+ isolationDir = await ensureWorktree(repoRoot, task.id);
812
+ await applyBaseline(isolationDir, taskBaseline);
813
+ }
814
+
799
815
  const result = await runSubprocess({
800
816
  cwd: this.session.cwd,
801
- worktree: worktreeDir,
817
+ worktree: isolationDir,
802
818
  agent,
803
819
  task: task.task,
804
820
  description: task.description,
@@ -830,13 +846,56 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
830
846
  preloadedSkills: task.preloadedSkills,
831
847
  promptTemplates,
832
848
  });
833
- const patch = await captureDeltaPatch(worktreeDir, baseline);
834
- const patchPath = path.join(effectiveArtifactsDir, `${task.id}.patch`);
835
- await Bun.write(patchPath, patch);
836
- return {
837
- ...result,
838
- patchPath,
839
- };
849
+ if (mergeMode === "branch" && result.exitCode === 0) {
850
+ try {
851
+ const commitMsg =
852
+ commitStyle === "ai" && this.session.modelRegistry
853
+ ? async (diff: string) => {
854
+ const smolModel = this.session.settings.getModelRole("smol");
855
+ return generateCommitMessage(
856
+ diff,
857
+ this.session.modelRegistry!,
858
+ smolModel,
859
+ this.session.getSessionId?.() ?? undefined,
860
+ );
861
+ }
862
+ : undefined;
863
+ const commitResult = await commitToBranch(
864
+ isolationDir,
865
+ taskBaseline,
866
+ task.id,
867
+ task.description,
868
+ commitMsg,
869
+ );
870
+ return {
871
+ ...result,
872
+ branchName: commitResult?.branchName,
873
+ nestedPatches: commitResult?.nestedPatches,
874
+ };
875
+ } catch (mergeErr) {
876
+ // Agent succeeded but branch commit failed — clean up stale branch
877
+ const branchName = `omp/task/${task.id}`;
878
+ await $`git branch -D ${branchName}`.cwd(repoRoot).quiet().nothrow();
879
+ const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
880
+ return { ...result, error: `Merge failed: ${msg}` };
881
+ }
882
+ }
883
+ if (result.exitCode === 0) {
884
+ try {
885
+ const delta = await captureDeltaPatch(isolationDir, taskBaseline);
886
+ const patchPath = path.join(effectiveArtifactsDir, `${task.id}.patch`);
887
+ await Bun.write(patchPath, delta.rootPatch);
888
+ return {
889
+ ...result,
890
+ patchPath,
891
+ nestedPatches: delta.nestedPatches,
892
+ };
893
+ } catch (patchErr) {
894
+ const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
895
+ return { ...result, error: `Patch capture failed: ${msg}` };
896
+ }
897
+ }
898
+ return result;
840
899
  } catch (err) {
841
900
  const message = err instanceof Error ? err.message : String(err);
842
901
  return {
@@ -856,8 +915,12 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
856
915
  error: message,
857
916
  };
858
917
  } finally {
859
- if (worktreeDir) {
860
- await cleanupWorktree(worktreeDir);
918
+ if (isolationDir) {
919
+ if (isolationMode === "fuse-overlay") {
920
+ await cleanupFuseOverlay(isolationDir);
921
+ } else {
922
+ await cleanupWorktree(isolationDir);
923
+ }
861
924
  }
862
925
  }
863
926
  };
@@ -917,74 +980,152 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
917
980
  }
918
981
  }
919
982
 
920
- let patchApplySummary = "";
921
- let patchesApplied: boolean | null = null;
922
- if (isIsolated) {
923
- const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
924
- const missingPatch = results.some(result => !result.patchPath);
925
- if (!repoRoot || missingPatch) {
926
- patchesApplied = false;
983
+ let mergeSummary = "";
984
+ let changesApplied: boolean | null = null;
985
+ let mergedBranchesForNestedPatches: Set<string> | null = null;
986
+ if (isIsolated && repoRoot) {
987
+ if (mergeMode === "branch") {
988
+ // Branch mode: merge task branches sequentially
989
+ const branchEntries = results
990
+ .filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
991
+ .map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
992
+
993
+ if (branchEntries.length === 0) {
994
+ changesApplied = true;
995
+ } else {
996
+ const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
997
+ mergedBranchesForNestedPatches = new Set(mergeResult.merged);
998
+ changesApplied = mergeResult.failed.length === 0;
999
+
1000
+ if (changesApplied) {
1001
+ mergeSummary = `\n\nMerged ${mergeResult.merged.length} branch${mergeResult.merged.length === 1 ? "" : "es"}: ${mergeResult.merged.join(", ")}`;
1002
+ } else {
1003
+ const mergedPart =
1004
+ mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
1005
+ const failedPart = `Failed: ${mergeResult.failed.join(", ")}.`;
1006
+ const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
1007
+ mergeSummary = `\n\n<system-notification>Branch merge failed. ${mergedPart}${failedPart}${conflictPart}\nUnmerged branches remain for manual resolution.</system-notification>`;
1008
+ }
1009
+ }
1010
+
1011
+ // Clean up merged branches (keep failed ones for manual resolution)
1012
+ const allBranches = branchEntries.map(b => b.branchName);
1013
+ if (changesApplied) {
1014
+ await cleanupTaskBranches(repoRoot, allBranches);
1015
+ }
927
1016
  } else {
928
- const patchStats = await Promise.all(
929
- patchesInOrder.map(async patchPath => ({
930
- patchPath,
931
- size: (await fs.stat(patchPath)).size,
932
- })),
933
- );
934
- const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
935
- if (nonEmptyPatches.length === 0) {
936
- patchesApplied = true;
1017
+ // Patch mode: combine and apply patches
1018
+ const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
1019
+ const missingPatch = results.some(result => !result.patchPath);
1020
+ if (missingPatch) {
1021
+ changesApplied = false;
937
1022
  } else {
938
- const patchTexts = await Promise.all(
939
- nonEmptyPatches.map(async patchPath => Bun.file(patchPath).text()),
1023
+ const patchStats = await Promise.all(
1024
+ patchesInOrder.map(async patchPath => ({
1025
+ patchPath,
1026
+ size: (await fs.stat(patchPath)).size,
1027
+ })),
940
1028
  );
941
- const combinedPatch = patchTexts.map(text => (text.endsWith("\n") ? text : `${text}\n`)).join("");
942
- if (!combinedPatch.trim()) {
943
- patchesApplied = true;
1029
+ const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
1030
+ if (nonEmptyPatches.length === 0) {
1031
+ changesApplied = true;
944
1032
  } else {
945
- const combinedPatchPath = path.join(os.tmpdir(), `omp-task-combined-${Snowflake.next()}.patch`);
946
- try {
947
- await Bun.write(combinedPatchPath, combinedPatch);
948
- const checkResult = await $`git apply --check --binary ${combinedPatchPath}`
949
- .cwd(repoRoot)
950
- .quiet()
951
- .nothrow();
952
- if (checkResult.exitCode !== 0) {
953
- patchesApplied = false;
954
- } else {
955
- const applyResult = await $`git apply --binary ${combinedPatchPath}`
1033
+ const patchTexts = await Promise.all(
1034
+ nonEmptyPatches.map(async patchPath => Bun.file(patchPath).text()),
1035
+ );
1036
+ const combinedPatch = patchTexts.map(text => (text.endsWith("\n") ? text : `${text}\n`)).join("");
1037
+ if (!combinedPatch.trim()) {
1038
+ changesApplied = true;
1039
+ } else {
1040
+ const combinedPatchPath = path.join(os.tmpdir(), `omp-task-combined-${Snowflake.next()}.patch`);
1041
+ try {
1042
+ await Bun.write(combinedPatchPath, combinedPatch);
1043
+ const checkResult = await $`git apply --check --binary ${combinedPatchPath}`
956
1044
  .cwd(repoRoot)
957
1045
  .quiet()
958
1046
  .nothrow();
959
- patchesApplied = applyResult.exitCode === 0;
1047
+ if (checkResult.exitCode !== 0) {
1048
+ changesApplied = false;
1049
+ } else {
1050
+ const applyResult = await $`git apply --binary ${combinedPatchPath}`
1051
+ .cwd(repoRoot)
1052
+ .quiet()
1053
+ .nothrow();
1054
+ changesApplied = applyResult.exitCode === 0;
1055
+ }
1056
+ } finally {
1057
+ await fs.rm(combinedPatchPath, { force: true });
960
1058
  }
961
- } finally {
962
- await fs.rm(combinedPatchPath, { force: true });
963
1059
  }
964
1060
  }
965
1061
  }
1062
+
1063
+ if (changesApplied) {
1064
+ mergeSummary = "\n\nApplied patches: yes";
1065
+ } else {
1066
+ const notification =
1067
+ "<system-notification>Patches were not applied and must be handled manually.</system-notification>";
1068
+ const patchList =
1069
+ patchPaths.length > 0
1070
+ ? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
1071
+ : "";
1072
+ mergeSummary = `\n\n${notification}${patchList}`;
1073
+ }
966
1074
  }
1075
+ }
967
1076
 
968
- if (patchesApplied) {
969
- patchApplySummary = "\n\nApplied patches: yes";
970
- } else {
971
- const notification =
972
- "<system-notification>Patches were not applied and must be handled manually.</system-notification>";
973
- const patchList =
974
- patchPaths.length > 0
975
- ? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
976
- : "";
977
- patchApplySummary = `\n\n${notification}${patchList}`;
1077
+ // Apply nested repo patches (separate from parent git)
1078
+ if (isIsolated && repoRoot && (mergeMode === "branch" || changesApplied !== false)) {
1079
+ const allNestedPatches = results
1080
+ .filter(r => {
1081
+ if (!r.nestedPatches || r.nestedPatches.length === 0 || r.exitCode !== 0 || r.aborted) {
1082
+ return false;
1083
+ }
1084
+ if (mergeMode !== "branch") {
1085
+ return true;
1086
+ }
1087
+ if (!r.branchName || !mergedBranchesForNestedPatches) {
1088
+ return false;
1089
+ }
1090
+ return mergedBranchesForNestedPatches.has(r.branchName);
1091
+ })
1092
+ .flatMap(r => r.nestedPatches!);
1093
+ if (allNestedPatches.length > 0) {
1094
+ try {
1095
+ const commitMsg =
1096
+ commitStyle === "ai" && this.session.modelRegistry
1097
+ ? async (diff: string) => {
1098
+ const smolModel = this.session.settings.getModelRole("smol");
1099
+ return generateCommitMessage(
1100
+ diff,
1101
+ this.session.modelRegistry!,
1102
+ smolModel,
1103
+ this.session.getSessionId?.() ?? undefined,
1104
+ );
1105
+ }
1106
+ : undefined;
1107
+ await applyNestedPatches(repoRoot, allNestedPatches, commitMsg);
1108
+ } catch {
1109
+ // Nested patch failures are non-fatal to the parent merge
1110
+ mergeSummary +=
1111
+ "\n\n<system-notification>Some nested repository patches failed to apply.</system-notification>";
1112
+ }
978
1113
  }
979
1114
  }
980
1115
 
981
1116
  // Build final output - match plugin format
982
- const successCount = results.filter(r => r.exitCode === 0).length;
1117
+ const successCount = results.filter(r => r.exitCode === 0 && !r.error).length;
983
1118
  const cancelledCount = results.filter(r => r.aborted).length;
984
1119
  const totalDuration = Date.now() - startTime;
985
1120
 
986
1121
  const summaries = results.map(r => {
987
- const status = r.aborted ? "cancelled" : r.exitCode === 0 ? "completed" : `failed (exit ${r.exitCode})`;
1122
+ const status = r.aborted
1123
+ ? "cancelled"
1124
+ : r.exitCode === 0 && r.error
1125
+ ? "merge failed"
1126
+ : r.exitCode === 0
1127
+ ? "completed"
1128
+ : `failed (exit ${r.exitCode})`;
988
1129
  const output = r.output.trim() || r.stderr.trim() || "(no output)";
989
1130
  const outputCharCount = r.outputMeta?.charCount ?? output.length;
990
1131
  const fullOutputThreshold = 5000;
@@ -1021,12 +1162,12 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
1021
1162
  summaries,
1022
1163
  outputIds,
1023
1164
  agentName,
1024
- patchApplySummary,
1165
+ mergeSummary,
1025
1166
  });
1026
1167
 
1027
1168
  // Cleanup temp directory if used
1028
1169
  const shouldCleanupTempArtifacts =
1029
- tempArtifactsDir && (!isIsolated || patchesApplied === true || patchesApplied === null);
1170
+ tempArtifactsDir && (!isIsolated || changesApplied === true || changesApplied === null);
1030
1171
  if (shouldCleanupTempArtifacts) {
1031
1172
  await fs.rm(tempArtifactsDir, { recursive: true, force: true });
1032
1173
  }
@@ -728,7 +728,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
728
728
  result.output,
729
729
  );
730
730
  const aborted = result.aborted ?? false;
731
- const success = !aborted && result.exitCode === 0;
731
+ const mergeFailed = !aborted && result.exitCode === 0 && !!result.error;
732
+ const success = !aborted && result.exitCode === 0 && !result.error;
732
733
  const needsWarning = Boolean(missingCompleteWarning) && success;
733
734
  const icon = aborted
734
735
  ? theme.status.aborted
@@ -737,8 +738,16 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
737
738
  : success
738
739
  ? theme.status.success
739
740
  : theme.status.error;
740
- const iconColor = needsWarning ? "warning" : success ? "success" : "error";
741
- const statusText = aborted ? "aborted" : needsWarning ? "warning" : success ? "done" : "failed";
741
+ const iconColor = needsWarning ? "warning" : success ? "success" : mergeFailed ? "warning" : "error";
742
+ const statusText = aborted
743
+ ? "aborted"
744
+ : needsWarning
745
+ ? "warning"
746
+ : success
747
+ ? "done"
748
+ : mergeFailed
749
+ ? "merge failed"
750
+ : "failed";
742
751
 
743
752
  // Main status line: id: description [status] · stats · ⟨agent⟩
744
753
  const description = result.description?.trim();
@@ -847,11 +856,13 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
847
856
 
848
857
  if (result.patchPath && !aborted && result.exitCode === 0) {
849
858
  lines.push(`${continuePrefix}${theme.fg("dim", `Patch: ${result.patchPath}`)}`);
859
+ } else if (result.branchName && !aborted && result.exitCode === 0) {
860
+ lines.push(`${continuePrefix}${theme.fg("dim", `Branch: ${result.branchName}`)}`);
850
861
  }
851
862
 
852
863
  // Error message
853
- if (result.error && !success) {
854
- lines.push(`${continuePrefix}${theme.fg("error", truncateToWidth(result.error, 70))}`);
864
+ if (result.error && (!success || mergeFailed)) {
865
+ lines.push(`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", truncateToWidth(result.error, 70))}`);
855
866
  }
856
867
 
857
868
  return lines;
@@ -902,15 +913,20 @@ export function renderResult(
902
913
  });
903
914
 
904
915
  const abortedCount = details.results.filter(r => r.aborted).length;
905
- const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0).length;
906
- const failCount = details.results.length - successCount - abortedCount;
916
+ const mergeFailedCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && r.error).length;
917
+ const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && !r.error).length;
918
+ const failCount = details.results.length - successCount - mergeFailedCount - abortedCount;
907
919
  let summary = `${theme.fg("dim", "Total:")} `;
908
920
  if (abortedCount > 0) {
909
921
  summary += theme.fg("error", `${abortedCount} aborted`);
910
- if (successCount > 0 || failCount > 0) summary += theme.sep.dot;
922
+ if (successCount > 0 || mergeFailedCount > 0 || failCount > 0) summary += theme.sep.dot;
911
923
  }
912
924
  if (successCount > 0) {
913
925
  summary += theme.fg("success", `${successCount} succeeded`);
926
+ if (mergeFailedCount > 0 || failCount > 0) summary += theme.sep.dot;
927
+ }
928
+ if (mergeFailedCount > 0) {
929
+ summary += theme.fg("warning", `${mergeFailedCount} merge failed`);
914
930
  if (failCount > 0) summary += theme.sep.dot;
915
931
  }
916
932
  if (failCount > 0) {
package/src/task/types.ts CHANGED
@@ -2,6 +2,7 @@ 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
4
  import { type Static, Type } from "@sinclair/typebox";
5
+ import type { NestedRepoPatch } from "./worktree";
5
6
 
6
7
  /** Source of an agent definition */
7
8
  export type AgentSource = "bundled" | "user" | "project";
@@ -77,7 +78,7 @@ const createTaskSchema = (options: { isolationEnabled: boolean }) => {
77
78
  ...properties,
78
79
  isolated: Type.Optional(
79
80
  Type.Boolean({
80
- description: "Run in isolated git worktree; returns patches. Use when tasks edit overlapping files.",
81
+ description: "Run in isolated environment; returns patches. Use when tasks edit overlapping files.",
81
82
  }),
82
83
  ),
83
84
  });
@@ -179,6 +180,10 @@ export interface SingleResult {
179
180
  outputPath?: string;
180
181
  /** Patch path for isolated worktree output */
181
182
  patchPath?: string;
183
+ /** Branch name for isolated branch-mode output */
184
+ branchName?: string;
185
+ /** Nested repo patches to apply after parent merge */
186
+ nestedPatches?: NestedRepoPatch[];
182
187
  /** Data extracted by registered subprocess tool handlers (keyed by tool name) */
183
188
  extractedToolData?: Record<string, unknown[]>;
184
189
  /** Output metadata for agent:// URL integration */