@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -6,12 +6,12 @@
6
6
  import path from "node:path";
7
7
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { SearchDb } from "@oh-my-pi/pi-natives";
9
- import { logger, untilAborted } from "@oh-my-pi/pi-utils";
9
+ import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import type { TSchema } from "@sinclair/typebox";
11
11
  import Ajv, { type ValidateFunction } from "ajv";
12
12
  import { ModelRegistry } from "../config/model-registry";
13
13
  import { resolveModelOverride } from "../config/model-resolver";
14
- import { type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
14
+ import type { PromptTemplate } from "../config/prompt-templates";
15
15
  import { Settings } from "../config/settings";
16
16
  import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
17
17
  import type { CustomTool } from "../extensibility/custom-tools/types";
@@ -38,6 +38,7 @@ import {
38
38
  type ReviewFinding,
39
39
  type SingleResult,
40
40
  TASK_SUBAGENT_EVENT_CHANNEL,
41
+ TASK_SUBAGENT_LIFECYCLE_CHANNEL,
41
42
  TASK_SUBAGENT_PROGRESS_CHANNEL,
42
43
  } from "./types";
43
44
 
@@ -630,6 +631,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
630
631
  task,
631
632
  assignment,
632
633
  progress: { ...progress },
634
+ sessionFile: subtaskSessionFile,
633
635
  });
634
636
  }
635
637
  lastProgressEmitMs = Date.now();
@@ -963,7 +965,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
963
965
  skills: options.skills,
964
966
  promptTemplates: options.promptTemplates,
965
967
  systemPrompt: defaultPrompt =>
966
- renderPromptTemplate(subagentSystemPromptTemplate, {
968
+ prompt.render(subagentSystemPromptTemplate, {
967
969
  base: defaultPrompt,
968
970
  agent: agent.systemPrompt,
969
971
  worktree: worktree ?? "",
@@ -983,6 +985,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
983
985
 
984
986
  activeSession = session;
985
987
 
988
+ // Emit lifecycle start event
989
+ if (options.eventBus) {
990
+ options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
991
+ id,
992
+ agent: agent.name,
993
+ agentSource: agent.source,
994
+ description: options.description,
995
+ status: "started",
996
+ sessionFile: subtaskSessionFile,
997
+ index,
998
+ });
999
+ }
1000
+
986
1001
  const subagentToolNames = session.getActiveToolNames();
987
1002
  const parentOwnedToolNames = new Set(["todo_write"]);
988
1003
  const filteredSubagentTools = subagentToolNames.filter(name => !parentOwnedToolNames.has(name));
@@ -1091,7 +1106,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1091
1106
  while (!submitResultCalled && retryCount < MAX_SUBMIT_RESULT_RETRIES && !abortSignal.aborted) {
1092
1107
  try {
1093
1108
  retryCount++;
1094
- const reminder = renderPromptTemplate(submitReminderTemplate, {
1109
+ const reminder = prompt.render(submitReminderTemplate, {
1095
1110
  retryCount,
1096
1111
  maxRetries: MAX_SUBMIT_RESULT_RETRIES,
1097
1112
  });
@@ -1238,6 +1253,19 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1238
1253
  progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
1239
1254
  scheduleProgress(true);
1240
1255
 
1256
+ // Emit lifecycle end event after finalization so submit_result status is reflected
1257
+ if (options.eventBus) {
1258
+ options.eventBus.emit(TASK_SUBAGENT_LIFECYCLE_CHANNEL, {
1259
+ id,
1260
+ agent: agent.name,
1261
+ agentSource: agent.source,
1262
+ description: options.description,
1263
+ status: progress.status as "completed" | "failed" | "aborted",
1264
+ sessionFile: subtaskSessionFile,
1265
+ index,
1266
+ });
1267
+ }
1268
+
1241
1269
  return {
1242
1270
  index,
1243
1271
  id,
package/src/task/index.ts CHANGED
@@ -17,11 +17,9 @@ import * as os from "node:os";
17
17
  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
- import { $env, Snowflake } from "@oh-my-pi/pi-utils";
21
- import { $ } from "bun";
20
+ import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
22
21
  import type { ToolSession } from "..";
23
22
  import { resolveAgentModelPatterns } from "../config/model-resolver";
24
- import { renderPromptTemplate } from "../config/prompt-templates";
25
23
  import type { Theme } from "../modes/theme/theme";
26
24
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
27
25
  import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
@@ -30,6 +28,7 @@ import { formatBytes, formatDuration } from "../tools/render-utils";
30
28
  // Import review tools for side effects (registers subagent tool handlers)
31
29
  import "../tools/review";
32
30
  import { generateCommitMessage } from "../utils/commit-message-generator";
31
+ import * as git from "../utils/git";
33
32
  import { discoverAgents, getAgent } from "./discovery";
34
33
  import { runSubprocess } from "./executor";
35
34
  import { resolveIsolationBackendForTaskExecution } from "./isolation-backend";
@@ -109,8 +108,21 @@ export { loadBundledAgents as BUNDLED_AGENTS } from "./agents";
109
108
  export { discoverCommands, expandCommand, getCommand } from "./commands";
110
109
  export { discoverAgents, getAgent } from "./discovery";
111
110
  export { AgentOutputManager } from "./output-manager";
112
- export type { AgentDefinition, AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
113
- export { taskSchema } from "./types";
111
+ export type {
112
+ AgentDefinition,
113
+ AgentProgress,
114
+ SingleResult,
115
+ SubagentLifecyclePayload,
116
+ SubagentProgressPayload,
117
+ TaskParams,
118
+ TaskToolDetails,
119
+ } from "./types";
120
+ export {
121
+ TASK_SUBAGENT_EVENT_CHANNEL,
122
+ TASK_SUBAGENT_LIFECYCLE_CHANNEL,
123
+ TASK_SUBAGENT_PROGRESS_CHANNEL,
124
+ taskSchema,
125
+ } from "./types";
114
126
 
115
127
  /**
116
128
  * Render the tool description from a cached agent list and current settings.
@@ -123,7 +135,7 @@ function renderDescription(
123
135
  disabledAgents: string[],
124
136
  ): string {
125
137
  const filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
126
- return renderPromptTemplate(taskDescriptionTemplate, {
138
+ return prompt.render(taskDescriptionTemplate, {
127
139
  agents: filteredAgents,
128
140
  MAX_CONCURRENCY: maxConcurrency,
129
141
  isolationEnabled,
@@ -766,7 +778,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
766
778
  contextFile: contextFilePath,
767
779
  enableLsp: false,
768
780
  signal,
769
- eventBus: undefined,
781
+ eventBus: this.session.eventBus,
770
782
  onProgress: progress => {
771
783
  progressMap.set(index, {
772
784
  ...structuredClone(progress),
@@ -820,7 +832,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
820
832
  contextFile: contextFilePath,
821
833
  enableLsp: false,
822
834
  signal,
823
- eventBus: undefined,
835
+ eventBus: this.session.eventBus,
824
836
  onProgress: progress => {
825
837
  progressMap.set(index, {
826
838
  ...structuredClone(progress),
@@ -864,7 +876,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
864
876
  } catch (mergeErr) {
865
877
  // Agent succeeded but branch commit failed — clean up stale branch
866
878
  const branchName = `omp/task/${task.id}`;
867
- await $`git branch -D ${branchName}`.cwd(repoRoot).quiet().nothrow();
879
+ await git.branch.tryDelete(repoRoot, branchName);
868
880
  const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
869
881
  return { ...result, error: `Merge failed: ${msg}` };
870
882
  }
@@ -978,93 +990,90 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
978
990
  let changesApplied: boolean | null = null;
979
991
  let mergedBranchesForNestedPatches: Set<string> | null = null;
980
992
  if (isIsolated && repoRoot) {
981
- if (mergeMode === "branch") {
982
- // Branch mode: merge task branches sequentially
983
- const branchEntries = results
984
- .filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
985
- .map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
986
-
987
- if (branchEntries.length === 0) {
988
- changesApplied = true;
989
- } else {
990
- const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
991
- mergedBranchesForNestedPatches = new Set(mergeResult.merged);
992
- changesApplied = mergeResult.failed.length === 0;
993
+ try {
994
+ if (mergeMode === "branch") {
995
+ // Branch mode: merge task branches sequentially
996
+ const branchEntries = results
997
+ .filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
998
+ .map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
993
999
 
994
- if (changesApplied) {
995
- mergeSummary = `\n\nMerged ${mergeResult.merged.length} branch${mergeResult.merged.length === 1 ? "" : "es"}: ${mergeResult.merged.join(", ")}`;
1000
+ if (branchEntries.length === 0) {
1001
+ changesApplied = true;
996
1002
  } else {
997
- const mergedPart =
998
- mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
999
- const failedPart = `Failed: ${mergeResult.failed.join(", ")}.`;
1000
- const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
1001
- mergeSummary = `\n\n<system-notification>Branch merge failed. ${mergedPart}${failedPart}${conflictPart}\nUnmerged branches remain for manual resolution.</system-notification>`;
1003
+ const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
1004
+ mergedBranchesForNestedPatches = new Set(mergeResult.merged);
1005
+ changesApplied = mergeResult.failed.length === 0;
1006
+
1007
+ if (changesApplied) {
1008
+ mergeSummary = `\n\nMerged ${mergeResult.merged.length} branch${mergeResult.merged.length === 1 ? "" : "es"}: ${mergeResult.merged.join(", ")}`;
1009
+ } else {
1010
+ const mergedPart =
1011
+ mergeResult.merged.length > 0 ? `Merged: ${mergeResult.merged.join(", ")}.\n` : "";
1012
+ const failedPart = `Failed: ${mergeResult.failed.join(", ")}.`;
1013
+ const conflictPart = mergeResult.conflict ? `\nConflict: ${mergeResult.conflict}` : "";
1014
+ mergeSummary = `\n\n<system-notification>Branch merge failed. ${mergedPart}${failedPart}${conflictPart}\nUnmerged branches remain for manual resolution.</system-notification>`;
1015
+ }
1002
1016
  }
1003
- }
1004
1017
 
1005
- // Clean up merged branches (keep failed ones for manual resolution)
1006
- const allBranches = branchEntries.map(b => b.branchName);
1007
- if (changesApplied) {
1008
- await cleanupTaskBranches(repoRoot, allBranches);
1009
- }
1010
- } else {
1011
- // Patch mode: combine and apply patches
1012
- const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
1013
- const missingPatch = results.some(result => !result.patchPath);
1014
- if (missingPatch) {
1015
- changesApplied = false;
1018
+ // Clean up merged branches (keep failed ones for manual resolution)
1019
+ const allBranches = branchEntries.map(b => b.branchName);
1020
+ if (changesApplied) {
1021
+ await cleanupTaskBranches(repoRoot, allBranches);
1022
+ }
1016
1023
  } else {
1017
- const patchStats = await Promise.all(
1018
- patchesInOrder.map(async patchPath => ({
1019
- patchPath,
1020
- size: (await fs.stat(patchPath)).size,
1021
- })),
1022
- );
1023
- const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
1024
- if (nonEmptyPatches.length === 0) {
1025
- changesApplied = true;
1024
+ // Patch mode: combine and apply patches
1025
+ const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
1026
+ const missingPatch = results.some(result => !result.patchPath);
1027
+ if (missingPatch) {
1028
+ changesApplied = false;
1026
1029
  } else {
1027
- const patchTexts = await Promise.all(
1028
- nonEmptyPatches.map(async patchPath => Bun.file(patchPath).text()),
1030
+ const patchStats = await Promise.all(
1031
+ patchesInOrder.map(async patchPath => ({
1032
+ patchPath,
1033
+ size: (await fs.stat(patchPath)).size,
1034
+ })),
1029
1035
  );
1030
- const combinedPatch = patchTexts.map(text => (text.endsWith("\n") ? text : `${text}\n`)).join("");
1031
- if (!combinedPatch.trim()) {
1036
+ const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
1037
+ if (nonEmptyPatches.length === 0) {
1032
1038
  changesApplied = true;
1033
1039
  } else {
1034
- const combinedPatchPath = path.join(os.tmpdir(), `omp-task-combined-${Snowflake.next()}.patch`);
1035
- try {
1036
- await Bun.write(combinedPatchPath, combinedPatch);
1037
- const checkResult = await $`git apply --check --binary ${combinedPatchPath}`
1038
- .cwd(repoRoot)
1039
- .quiet()
1040
- .nothrow();
1041
- if (checkResult.exitCode !== 0) {
1042
- changesApplied = false;
1043
- } else {
1044
- const applyResult = await $`git apply --binary ${combinedPatchPath}`
1045
- .cwd(repoRoot)
1046
- .quiet()
1047
- .nothrow();
1048
- changesApplied = applyResult.exitCode === 0;
1040
+ const patchTexts = await Promise.all(
1041
+ nonEmptyPatches.map(async patchPath => Bun.file(patchPath).text()),
1042
+ );
1043
+ const combinedPatch = patchTexts
1044
+ .map(text => (text.endsWith("\n") ? text : `${text}\n`))
1045
+ .join("");
1046
+ if (!combinedPatch.trim()) {
1047
+ changesApplied = true;
1048
+ } else {
1049
+ changesApplied = await git.patch.canApplyText(repoRoot, combinedPatch);
1050
+ if (changesApplied) {
1051
+ try {
1052
+ await git.patch.applyText(repoRoot, combinedPatch);
1053
+ } catch {
1054
+ changesApplied = false;
1055
+ }
1049
1056
  }
1050
- } finally {
1051
- await fs.rm(combinedPatchPath, { force: true });
1052
1057
  }
1053
1058
  }
1054
1059
  }
1055
- }
1056
1060
 
1057
- if (changesApplied) {
1058
- mergeSummary = "\n\nApplied patches: yes";
1059
- } else {
1060
- const notification =
1061
- "<system-notification>Patches were not applied and must be handled manually.</system-notification>";
1062
- const patchList =
1063
- patchPaths.length > 0
1064
- ? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
1065
- : "";
1066
- mergeSummary = `\n\n${notification}${patchList}`;
1061
+ if (changesApplied) {
1062
+ mergeSummary = "\n\nApplied patches: yes";
1063
+ } else {
1064
+ const notification =
1065
+ "<system-notification>Patches were not applied and must be handled manually.</system-notification>";
1066
+ const patchList =
1067
+ patchPaths.length > 0
1068
+ ? `\n\nPatch artifacts:\n${patchPaths.map(patch => `- ${patch}`).join("\n")}`
1069
+ : "";
1070
+ mergeSummary = `\n\n${notification}${patchList}`;
1071
+ }
1067
1072
  }
1073
+ } catch (mergeErr) {
1074
+ const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
1075
+ changesApplied = false;
1076
+ mergeSummary = `\n\n<system-notification>Merge phase failed: ${msg}\nTask outputs are preserved but changes were not applied.</system-notification>`;
1068
1077
  }
1069
1078
  }
1070
1079
 
@@ -1147,7 +1156,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
1147
1156
 
1148
1157
  const outputIds = results.filter(r => !r.aborted || r.output.trim()).map(r => `agent://${r.id}`);
1149
1158
  const backendSummaryPrefix = isolationBackendWarning ? `\n\n${isolationBackendWarning}` : "";
1150
- const summary = renderPromptTemplate(taskSummaryTemplate, {
1159
+ const summary = prompt.render(taskSummaryTemplate, {
1151
1160
  successCount,
1152
1161
  totalCount: results.length,
1153
1162
  cancelledCount,
@@ -1,4 +1,4 @@
1
- import { renderPromptTemplate } from "../config/prompt-templates";
1
+ import { prompt } from "@oh-my-pi/pi-utils";
2
2
  import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
3
3
  import type { TaskItem } from "./types";
4
4
 
@@ -25,7 +25,7 @@ export function renderTemplate(context: string | undefined, task: TaskItem): Ren
25
25
  return { task: assignment || context!, assignment: assignment || context!, id, description };
26
26
  }
27
27
  return {
28
- task: renderPromptTemplate(subagentUserPromptTemplate, { context, assignment }),
28
+ task: prompt.render(subagentUserPromptTemplate, { context, assignment }),
29
29
  assignment,
30
30
  id,
31
31
  description,
package/src/task/types.ts CHANGED
@@ -31,6 +31,31 @@ export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
31
31
  /** EventBus channel for aggregated subagent progress */
32
32
  export const TASK_SUBAGENT_PROGRESS_CHANNEL = "task:subagent:progress";
33
33
 
34
+ /** EventBus channel for subagent lifecycle (start/end) */
35
+ export const TASK_SUBAGENT_LIFECYCLE_CHANNEL = "task:subagent:lifecycle";
36
+
37
+ /** Payload emitted on TASK_SUBAGENT_PROGRESS_CHANNEL */
38
+ export interface SubagentProgressPayload {
39
+ index: number;
40
+ agent: string;
41
+ agentSource: AgentSource;
42
+ task: string;
43
+ assignment?: string;
44
+ progress: AgentProgress;
45
+ sessionFile?: string;
46
+ }
47
+
48
+ /** Payload emitted on TASK_SUBAGENT_LIFECYCLE_CHANNEL */
49
+ export interface SubagentLifecyclePayload {
50
+ id: string;
51
+ agent: string;
52
+ agentSource: AgentSource;
53
+ description?: string;
54
+ status: "started" | "completed" | "failed" | "aborted";
55
+ sessionFile?: string;
56
+ index: number;
57
+ }
58
+
34
59
  /** Single task item for parallel execution */
35
60
  export const taskItemSchema = Type.Object({
36
61
  id: Type.String({