@gajae-code/coding-agent 0.12.4 → 0.12.6

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 (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -1
  3. package/dist/types/async/job-manager.d.ts +9 -1
  4. package/dist/types/eval/backend.d.ts +2 -2
  5. package/dist/types/exec/bash-executor.d.ts +10 -8
  6. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  7. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  8. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  9. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  10. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  11. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  12. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  15. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  16. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  17. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  19. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  20. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  21. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  22. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  23. package/dist/types/sdk/broker/broker.d.ts +18 -0
  24. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  25. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  26. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
  27. package/dist/types/sdk/bus/index.d.ts +4 -0
  28. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
  29. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  30. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  31. package/dist/types/sdk/host/host.d.ts +23 -2
  32. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  33. package/dist/types/session/agent-session.d.ts +5 -4
  34. package/dist/types/session/artifacts.d.ts +8 -5
  35. package/dist/types/session/attempt-record-store.d.ts +19 -0
  36. package/dist/types/session/client-bridge.d.ts +14 -0
  37. package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
  38. package/dist/types/session/messages.d.ts +4 -1
  39. package/dist/types/session/session-manager.d.ts +7 -5
  40. package/dist/types/session/session-storage.d.ts +8 -0
  41. package/dist/types/session/streaming-output.d.ts +32 -0
  42. package/dist/types/task/executor.d.ts +1 -1
  43. package/dist/types/task/index.d.ts +2 -1
  44. package/dist/types/task/receipt.d.ts +3 -0
  45. package/dist/types/task/types.d.ts +194 -0
  46. package/dist/types/task/worktree.d.ts +13 -6
  47. package/dist/types/tools/bash.d.ts +1 -1
  48. package/dist/types/tools/fetch.d.ts +3 -0
  49. package/dist/types/tools/output-meta.d.ts +37 -5
  50. package/dist/types/tools/subagent.d.ts +2 -0
  51. package/dist/types/tools/tool-result.d.ts +1 -0
  52. package/dist/types/utils/artifact-id.d.ts +2 -0
  53. package/dist/types/utils/git.d.ts +1 -0
  54. package/package.json +8 -8
  55. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  56. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  57. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  58. package/src/async/job-manager.ts +13 -5
  59. package/src/config/model-profiles.ts +8 -0
  60. package/src/config/settings-schema.ts +17 -3
  61. package/src/cursor.ts +20 -13
  62. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  63. package/src/eval/backend.ts +2 -2
  64. package/src/exec/bash-executor.ts +425 -151
  65. package/src/extensibility/extensions/runner.ts +80 -6
  66. package/src/extensibility/extensions/wrapper.ts +22 -15
  67. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  68. package/src/gjc-runtime/launch-tmux.ts +309 -293
  69. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  70. package/src/gjc-runtime/psmux-detect.ts +52 -6
  71. package/src/gjc-runtime/team-launch.ts +50 -2
  72. package/src/gjc-runtime/team-runtime.ts +716 -251
  73. package/src/gjc-runtime/team-store.ts +25 -7
  74. package/src/gjc-runtime/team-workers.ts +161 -1
  75. package/src/gjc-runtime/tmux-common.ts +16 -4
  76. package/src/gjc-runtime/tmux-gc.ts +37 -5
  77. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  78. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  79. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  80. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  81. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  82. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  83. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  84. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  85. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  86. package/src/internal-urls/agent-protocol.ts +98 -38
  87. package/src/internal-urls/artifact-protocol.ts +28 -17
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/internal-urls/local-protocol.ts +39 -18
  90. package/src/internal-urls/registry-helpers.ts +62 -0
  91. package/src/modes/acp/acp-agent.ts +51 -13
  92. package/src/modes/acp/acp-client-bridge.ts +154 -11
  93. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  94. package/src/modes/components/plugin-settings.ts +5 -0
  95. package/src/modes/components/settings-selector.ts +4 -0
  96. package/src/modes/controllers/selector-controller.ts +1 -0
  97. package/src/prompts/tools/bash.md +2 -2
  98. package/src/prompts/tools/monitor.md +4 -4
  99. package/src/sdk/acp/adapter.ts +200 -9
  100. package/src/sdk/broker/broker.ts +93 -10
  101. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  102. package/src/sdk/broker/lifecycle.ts +692 -98
  103. package/src/sdk/bus/chat-daemon-control.ts +33 -5
  104. package/src/sdk/bus/index.ts +347 -14
  105. package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
  106. package/src/sdk/bus/telegram-daemon.ts +283 -44
  107. package/src/sdk/bus/topic-registry.ts +133 -10
  108. package/src/sdk/host/host.ts +187 -9
  109. package/src/sdk/host/reverse-leases.ts +249 -42
  110. package/src/sdk/session.ts +8 -6
  111. package/src/session/agent-session.ts +930 -354
  112. package/src/session/artifacts.ts +211 -74
  113. package/src/session/attempt-record-store.ts +106 -0
  114. package/src/session/client-bridge.ts +15 -0
  115. package/src/session/internal/managed-session-scope.ts +332 -198
  116. package/src/session/internal/managed-session-storage.ts +162 -450
  117. package/src/session/messages.ts +32 -7
  118. package/src/session/session-manager.ts +31 -28
  119. package/src/session/session-storage.ts +190 -53
  120. package/src/session/streaming-output.ts +198 -40
  121. package/src/task/executor.ts +5 -2
  122. package/src/task/index.ts +277 -64
  123. package/src/task/provider-retry-status.ts +4 -1
  124. package/src/task/receipt.ts +26 -2
  125. package/src/task/render.ts +42 -1
  126. package/src/task/types.ts +52 -0
  127. package/src/task/worktree.ts +122 -43
  128. package/src/tools/bash.ts +818 -239
  129. package/src/tools/eval.ts +99 -4
  130. package/src/tools/fetch.ts +77 -15
  131. package/src/tools/find.ts +7 -3
  132. package/src/tools/monitor.ts +7 -1
  133. package/src/tools/output-meta.ts +706 -116
  134. package/src/tools/search.ts +9 -4
  135. package/src/tools/subagent-render.ts +5 -2
  136. package/src/tools/subagent.ts +8 -0
  137. package/src/tools/tool-result.ts +1 -0
  138. package/src/utils/artifact-id.ts +6 -0
  139. package/src/utils/git.ts +2 -0
package/src/task/index.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * - Progress tracking via JSON events
13
13
  * - Session artifacts for debugging
14
14
  */
15
- import { randomUUID } from "node:crypto";
15
+ import { createHash, randomUUID } from "node:crypto";
16
16
  import * as fs from "node:fs/promises";
17
17
  import * as os from "node:os";
18
18
  import path from "node:path";
@@ -53,6 +53,9 @@ import {
53
53
  RepositoryBindingError,
54
54
  resolveTaskRepositoryBinding,
55
55
  } from "../gjc-runtime/repository-binding";
56
+ import { reconcileReviewSourceDelivery } from "../gjc-runtime/ultragoal-review-source";
57
+ import { classifyUltragoalReviewDelivery, validateUltragoalReviewDispatch } from "../gjc-runtime/ultragoal-runtime";
58
+
56
59
  import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
57
60
  import { ArtifactManager } from "../session/artifacts";
58
61
  import { generateCommitMessage } from "../utils/commit-message-generator";
@@ -64,7 +67,7 @@ import { FORK_CONTEXT_TOKEN_BUDGET_BY_MODE } from "./fork-context-budget";
64
67
  import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
65
68
  import { AgentOutputManager } from "./output-manager";
66
69
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
67
- import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary } from "./receipt";
70
+ import { assertNoRawTaskFields, buildTaskReceipt, buildTaskRoiSummary, type TaskResultReceipt } from "./receipt";
68
71
 
69
72
  import { renderResult, renderCall as renderTaskCall } from "./render";
70
73
  import { reconcileSpawnRoi } from "./roi-reconciliation";
@@ -82,6 +85,9 @@ import {
82
85
  type IsolationHandle,
83
86
  mergeTaskBranches,
84
87
  parseIsolationMode,
88
+ serializeRecoveryPatchBundle,
89
+ verifyNestedPatchesApplied,
90
+ verifyRootPatchesApplied,
85
91
  type WorktreeBaseline,
86
92
  } from "./worktree";
87
93
 
@@ -104,9 +110,22 @@ function renderTaskAssignment(assignment: string, simpleMode: TaskSimpleMode): s
104
110
 
105
111
  export function subagentRunOutcomeFromSingleResult(
106
112
  finalText: string,
107
- singleResult: Pick<SingleResult, "aborted" | "exitCode" | "paused" | "setupFailure"> | undefined,
113
+ singleResult:
114
+ | (Pick<SingleResult, "aborted" | "exitCode" | "paused" | "setupFailure"> &
115
+ Partial<Pick<TaskResultReceipt, "status">>)
116
+ | undefined,
108
117
  ): string | SubagentRunOutcome {
109
118
  if (singleResult?.paused) return { kind: "paused" };
119
+ if (!singleResult) return { kind: "failed", text: finalText };
120
+ if (singleResult?.status && singleResult.status !== "completed") {
121
+ return {
122
+ kind: "failed",
123
+ text: finalText,
124
+ ...(singleResult.setupFailure && !singleResult.aborted
125
+ ? { setupFailureSummary: singleResult.setupFailure.summary }
126
+ : {}),
127
+ };
128
+ }
110
129
  if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
111
130
  return {
112
131
  kind: "failed",
@@ -1042,7 +1061,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1042
1061
  progress.setupFailure = singleResult?.setupFailure;
1043
1062
  }
1044
1063
  completedJobs += 1;
1045
- if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
1064
+ if (singleResult?.status !== "completed") {
1046
1065
  failedJobs += 1;
1047
1066
  }
1048
1067
  const remaining = taskItems.length - completedJobs;
@@ -1157,6 +1176,11 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1157
1176
  }
1158
1177
  }
1159
1178
 
1179
+ if (failedSchedules.length > 0) {
1180
+ completedJobs += failedSchedules.length;
1181
+ failedJobs += failedSchedules.length;
1182
+ }
1183
+
1160
1184
  if (startedJobs.length === 0) {
1161
1185
  const failureText = `Failed to start background task jobs: ${failedSchedules.join("; ")}`;
1162
1186
  return {
@@ -1165,9 +1189,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1165
1189
  };
1166
1190
  }
1167
1191
 
1192
+ const asyncState = completedJobs === taskItems.length ? (failedJobs > 0 ? "failed" : "completed") : "running";
1168
1193
  emitAsyncUpdate(
1169
- "running",
1170
- `Launching ${startedJobs.length} background ${startedJobs.length === 1 ? "task" : "tasks"}...`,
1194
+ asyncState,
1195
+ asyncState === "running"
1196
+ ? `Launching ${startedJobs.length} background ${startedJobs.length === 1 ? "task" : "tasks"}...`
1197
+ : asyncState === "completed"
1198
+ ? `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`
1199
+ : `Background task batch complete with failures: ${failedJobs} failed.`,
1171
1200
  );
1172
1201
 
1173
1202
  const scheduleFailureSummary =
@@ -1196,7 +1225,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1196
1225
  content: [
1197
1226
  {
1198
1227
  type: "text",
1199
- text: `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.\n${startedListing}\n${coordinationHint}`,
1228
+ text:
1229
+ asyncState === "running"
1230
+ ? `Started ${startedJobs.length} background task job${startedJobs.length === 1 ? "" : "s"} using ${params.agent}.${scheduleFailureSummary} Results will be delivered when complete.\n${startedListing}\n${coordinationHint}`
1231
+ : `Background task batch ${asyncState}: ${completedJobs}/${taskItems.length} finished.${scheduleFailureSummary}\n${startedListing}`,
1200
1232
  },
1201
1233
  ],
1202
1234
  details: {
@@ -1204,7 +1236,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1204
1236
  results: [],
1205
1237
  totalDurationMs: 0,
1206
1238
  progress: getProgressSnapshot(),
1207
- async: { state: "running", jobId: startedJobs[0].jobId, type: "task" },
1239
+ async: { state: asyncState, jobId: startedJobs[0].jobId, type: "task" },
1208
1240
  },
1209
1241
  };
1210
1242
  }
@@ -1673,6 +1705,27 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1673
1705
  if (taskRepositoryBinding.relativeSubdir) {
1674
1706
  assertPathUnderRepositoryBinding(taskRepositoryBinding, ".");
1675
1707
  }
1708
+ if (task.reviewSource) {
1709
+ await validateUltragoalReviewDispatch({
1710
+ cwd: this.session.cwd,
1711
+ dispatchId: task.reviewSource.dispatchId,
1712
+ cohortId: task.reviewSource.cohortId,
1713
+ taskId: task.id,
1714
+ lane: task.reviewSource.lane,
1715
+ snapshotId: task.reviewSource.snapshotId,
1716
+ generation: task.reviewSource.generation,
1717
+ repositoryBindingDigest: task.reviewSource.repositoryBindingDigest,
1718
+ stateRevision: task.reviewSource.stateRevision,
1719
+ rerunCommand: task.reviewSource.rerunCommand,
1720
+ taskSourceTaskId: task.reviewSource.taskId,
1721
+ createdAt: task.reviewSource.createdAt,
1722
+ });
1723
+ }
1724
+ if (isIsolated && task.reviewSource) {
1725
+ throw new Error(
1726
+ "source-aware review tasks cannot use generic task isolation; dispatch them against the coordinator-owned source snapshot",
1727
+ );
1728
+ }
1676
1729
  if (!isIsolated) {
1677
1730
  await assertExecutionRootMatchesRepositoryBinding(this.session.cwd, taskRepositoryBinding);
1678
1731
  const result = await runSubprocess({
@@ -1727,11 +1780,32 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1727
1780
  parentTelemetry: this.session.getTelemetry?.(),
1728
1781
  forkContextSeed,
1729
1782
  });
1783
+ const reviewSourceDisposition = task.reviewSource
1784
+ ? await reconcileReviewSourceDelivery({
1785
+ cwd: this.session.cwd,
1786
+ repositoryBinding: taskRepositoryBinding,
1787
+ reviewSource: task.reviewSource,
1788
+ })
1789
+ : undefined;
1790
+ const reviewSource =
1791
+ task.reviewSource && reviewSourceDisposition
1792
+ ? {
1793
+ ...task.reviewSource,
1794
+ ...reviewSourceDisposition,
1795
+ ...(await classifyUltragoalReviewDelivery({
1796
+ cwd: this.session.cwd,
1797
+ cohortId: task.reviewSource.cohortId,
1798
+ dispatchId: task.reviewSource.dispatchId,
1799
+ observedDisposition: reviewSourceDisposition.disposition,
1800
+ })),
1801
+ }
1802
+ : undefined;
1730
1803
  return {
1731
1804
  ...result,
1732
1805
  ...(forkContext ? { forkContext } : {}),
1733
1806
  forkContextAdvisory,
1734
1807
  repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
1808
+ ...(reviewSource ? { reviewSource } : {}),
1735
1809
  };
1736
1810
  }
1737
1811
 
@@ -1801,13 +1875,18 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1801
1875
  parentTelemetry: this.session.getTelemetry?.(),
1802
1876
  forkContextSeed,
1803
1877
  });
1804
- const resultWithForkContext = {
1878
+ let capturedResult = {
1805
1879
  ...result,
1806
1880
  ...(forkContext ? { forkContext } : {}),
1807
1881
  forkContextAdvisory,
1808
1882
  repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
1809
1883
  };
1810
- if (mergeMode === "branch" && resultWithForkContext.exitCode === 0) {
1884
+ const taskSucceeded =
1885
+ capturedResult.exitCode === 0 &&
1886
+ !capturedResult.error &&
1887
+ !capturedResult.aborted &&
1888
+ !capturedResult.paused;
1889
+ if (mergeMode === "branch" && taskSucceeded) {
1811
1890
  try {
1812
1891
  const commitMsg =
1813
1892
  commitStyle === "ai" && this.session.modelRegistry
@@ -1828,8 +1907,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1828
1907
  commitMsg,
1829
1908
  );
1830
1909
  const producedChanges = Boolean(commitResult?.branchName || commitResult?.nestedPatches.length);
1831
- return {
1832
- ...resultWithForkContext,
1910
+ capturedResult = {
1911
+ ...capturedResult,
1833
1912
  branchName: commitResult?.branchName,
1834
1913
  nestedPatches: commitResult?.nestedPatches,
1835
1914
  producedChanges,
@@ -1839,34 +1918,51 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1839
1918
  const branchName = `gjc/task/${task.id}`;
1840
1919
  await git.branch.tryDelete(repoRoot, branchName);
1841
1920
  const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
1842
- return { ...resultWithForkContext, error: `Merge failed: ${msg}` };
1921
+ capturedResult = { ...capturedResult, error: `Merge failed: ${msg}` };
1843
1922
  }
1844
1923
  }
1845
- if (resultWithForkContext.exitCode === 0) {
1846
- try {
1847
- const delta = await captureDeltaPatch(isolationDir, taskBaseline);
1848
- await initializeLocalRoot(localProtocolOptions);
1849
- const artifactId = validateAllocatedTaskId(task.id);
1850
- const patchPath = resolveLocalUrlToPath(
1851
- `local://subagents/${artifactId}.patch`,
1852
- localProtocolOptions,
1853
- );
1854
- await fs.mkdir(path.dirname(patchPath), { recursive: true, mode: 0o700 });
1855
- await Bun.write(patchPath, delta.rootPatch);
1856
- isolatedPatchBytes.set(patchPath, Buffer.from(delta.rootPatch, "utf8"));
1857
- const producedChanges = Boolean(delta.rootPatch.trim() || delta.nestedPatches.length);
1858
- return {
1859
- ...resultWithForkContext,
1860
- patchPath,
1861
- nestedPatches: delta.nestedPatches,
1862
- producedChanges,
1863
- };
1864
- } catch (patchErr) {
1865
- const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
1866
- return { ...resultWithForkContext, error: `Patch capture failed: ${msg}` };
1867
- }
1924
+ try {
1925
+ const delta = await captureDeltaPatch(isolationDir, taskBaseline);
1926
+ await initializeLocalRoot(localProtocolOptions);
1927
+ const artifactId = validateAllocatedTaskId(task.id);
1928
+ const patchUri = `local://subagents/${artifactId}-${Snowflake.next()}.patch`;
1929
+ const patchPath = resolveLocalUrlToPath(patchUri, localProtocolOptions);
1930
+ const rootPatchBytes = Buffer.from(delta.rootPatch, "utf8");
1931
+ const recoveryBytes = Buffer.from(serializeRecoveryPatchBundle(delta), "utf8");
1932
+ await fs.mkdir(path.dirname(patchPath), { recursive: true, mode: 0o700 });
1933
+ await Bun.write(patchPath, recoveryBytes);
1934
+ isolatedPatchBytes.set(patchPath, rootPatchBytes);
1935
+ const producedChanges = Boolean(
1936
+ delta.rootPatch.trim() || delta.nestedPatches.length || delta.captureErrors?.length,
1937
+ );
1938
+ const recoveryRef =
1939
+ recoveryBytes.byteLength > 0
1940
+ ? {
1941
+ uri: patchUri,
1942
+ sizeBytes: recoveryBytes.byteLength,
1943
+ sha256: createHash("sha256").update(recoveryBytes).digest("hex"),
1944
+ durability: "session" as const,
1945
+ }
1946
+ : undefined;
1947
+ const captureError = delta.captureErrors?.length
1948
+ ? `Patch capture incomplete: ${delta.captureErrors.join("; ")}`
1949
+ : undefined;
1950
+ const resultError = captureError
1951
+ ? `${capturedResult.error ? `${capturedResult.error}; ` : ""}${captureError}`
1952
+ : capturedResult.error;
1953
+ return {
1954
+ ...capturedResult,
1955
+ ...(resultError ? { error: resultError } : {}),
1956
+ patchPath,
1957
+ nestedPatches: delta.nestedPatches,
1958
+ producedChanges,
1959
+ recoveryRef,
1960
+ };
1961
+ } catch (patchErr) {
1962
+ const msg = patchErr instanceof Error ? patchErr.message : String(patchErr);
1963
+ const existingError = capturedResult.error ? `${capturedResult.error}; ` : "";
1964
+ return { ...capturedResult, error: `${existingError}Patch capture failed: ${msg}` };
1868
1965
  }
1869
- return resultWithForkContext;
1870
1966
  } catch (err) {
1871
1967
  const message = err instanceof Error ? err.message : String(err);
1872
1968
  const assignment = task.assignment.trim();
@@ -1966,13 +2062,23 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1966
2062
  let mergeSummary = "";
1967
2063
  let changesApplied: boolean | null = null;
1968
2064
  let hadAnyChanges = false;
2065
+ let rootPatchTexts: string[] = [];
1969
2066
  let mergedBranchesForNestedPatches: Set<string> | null = null;
2067
+ let mergePhaseFailed = false;
2068
+ const setRecoveryAvailable = (result: SingleResult): void => {
2069
+ if (!result.recoveryRef) return;
2070
+ result.persistence = {
2071
+ outcome: "recovery_available",
2072
+ ownerWorktreeApplied: false,
2073
+ recoveryRef: result.recoveryRef,
2074
+ };
2075
+ };
1970
2076
  if (isIsolated && repoRoot) {
1971
2077
  try {
1972
2078
  if (mergeMode === "branch") {
1973
2079
  // Branch mode: merge task branches sequentially
1974
2080
  const branchEntries = results
1975
- .filter(r => r.branchName && r.exitCode === 0 && !r.aborted)
2081
+ .filter(r => r.branchName && r.exitCode === 0 && !r.error && !r.aborted && !r.paused)
1976
2082
  .map(r => ({ branchName: r.branchName!, taskId: r.id, description: r.description }));
1977
2083
 
1978
2084
  if (branchEntries.length === 0) {
@@ -1981,7 +2087,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1981
2087
  mergeSummary = "\n\nNo changes to apply.";
1982
2088
  } else {
1983
2089
  const mergeResult = await mergeTaskBranches(repoRoot, branchEntries);
1984
- mergedBranchesForNestedPatches = new Set(mergeResult.merged);
2090
+ mergedBranchesForNestedPatches = new Set(
2091
+ mergeResult.merged.filter(branchName => !mergeResult.failed.includes(branchName)),
2092
+ );
1985
2093
  changesApplied = mergeResult.failed.length === 0;
1986
2094
  hadAnyChanges = changesApplied && mergeResult.merged.length > 0;
1987
2095
 
@@ -1998,15 +2106,48 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1998
2106
  }
1999
2107
  }
2000
2108
 
2109
+ for (const result of results) {
2110
+ const hasChanges = Boolean(
2111
+ result.branchName || result.nestedPatches?.length || result.recoveryRef,
2112
+ );
2113
+ if (!hasChanges) {
2114
+ if (result.exitCode === 0 && !result.error && !result.aborted && !result.paused) {
2115
+ result.persistence = { outcome: "no_changes", ownerWorktreeApplied: true };
2116
+ }
2117
+ continue;
2118
+ }
2119
+ if (
2120
+ result.branchName &&
2121
+ mergedBranchesForNestedPatches?.has(result.branchName) &&
2122
+ result.exitCode === 0 &&
2123
+ !result.error &&
2124
+ !result.aborted &&
2125
+ !result.paused
2126
+ ) {
2127
+ result.persistence = {
2128
+ outcome: "applied",
2129
+ ownerWorktreeApplied: true,
2130
+ recoveryRef: result.recoveryRef,
2131
+ };
2132
+ } else {
2133
+ setRecoveryAvailable(result);
2134
+ }
2135
+ }
2136
+
2001
2137
  // Clean up merged branches (keep failed ones for manual resolution)
2002
2138
  const allBranches = branchEntries.map(b => b.branchName);
2003
2139
  if (changesApplied) {
2004
2140
  await cleanupTaskBranches(repoRoot, allBranches);
2005
2141
  }
2006
2142
  } else {
2007
- // Patch mode: combine and apply patches
2008
- const patchesInOrder = results.map(result => result.patchPath).filter(Boolean) as string[];
2009
- const missingPatch = results.some(result => !result.patchPath);
2143
+ // Patch mode: combine and apply only successful task patches. Failed/interrupted patches remain recovery-only.
2144
+ const successfulPatchResults = results.filter(
2145
+ result => result.exitCode === 0 && !result.error && !result.aborted && !result.paused,
2146
+ );
2147
+ const patchesInOrder = successfulPatchResults
2148
+ .map(result => result.patchPath)
2149
+ .filter(Boolean) as string[];
2150
+ const missingPatch = successfulPatchResults.some(result => !result.patchPath);
2010
2151
  if (missingPatch) {
2011
2152
  changesApplied = false;
2012
2153
  hadAnyChanges = false;
@@ -2023,10 +2164,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2023
2164
  changesApplied = true;
2024
2165
  hadAnyChanges = false;
2025
2166
  } else {
2026
- const patchTexts = nonEmptyPatches.map(patchPath =>
2167
+ rootPatchTexts = nonEmptyPatches.map(patchPath =>
2027
2168
  isolatedPatchBytes.get(patchPath)!.toString("utf8"),
2028
2169
  );
2029
- const combinedPatch = patchTexts
2170
+ const combinedPatch = rootPatchTexts
2030
2171
  .map(text => (text.endsWith("\n") ? text : `${text}\n`))
2031
2172
  .join("");
2032
2173
  if (!combinedPatch.trim()) {
@@ -2047,6 +2188,48 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2047
2188
  }
2048
2189
  }
2049
2190
 
2191
+ if (changesApplied && rootPatchTexts.length > 0 && baseline) {
2192
+ changesApplied = await verifyRootPatchesApplied(repoRoot, baseline, rootPatchTexts);
2193
+ }
2194
+
2195
+ for (const result of results) {
2196
+ const patchBytes = result.patchPath ? isolatedPatchBytes.get(result.patchPath) : undefined;
2197
+ const hasChanges = Boolean(
2198
+ patchBytes?.toString("utf8").trim() ||
2199
+ (result.nestedPatches && result.nestedPatches.length > 0) ||
2200
+ result.recoveryRef,
2201
+ );
2202
+ if (!hasChanges) {
2203
+ if (result.exitCode === 0 && !result.error && !result.aborted && !result.paused) {
2204
+ result.persistence = {
2205
+ outcome: "no_changes",
2206
+ ownerWorktreeApplied: true,
2207
+ };
2208
+ }
2209
+ continue;
2210
+ }
2211
+ if (
2212
+ changesApplied &&
2213
+ patchBytes &&
2214
+ result.exitCode === 0 &&
2215
+ !result.error &&
2216
+ !result.aborted &&
2217
+ !result.paused
2218
+ ) {
2219
+ result.persistence = {
2220
+ outcome: "applied",
2221
+ ownerWorktreeApplied: true,
2222
+ recoveryRef: result.recoveryRef,
2223
+ };
2224
+ } else {
2225
+ result.persistence = {
2226
+ outcome: "recovery_available",
2227
+ ownerWorktreeApplied: false,
2228
+ recoveryRef: result.recoveryRef,
2229
+ };
2230
+ }
2231
+ }
2232
+
2050
2233
  if (changesApplied) {
2051
2234
  mergeSummary = hadAnyChanges ? "\n\nApplied patches: yes" : "\n\nNo changes to apply.";
2052
2235
  } else {
@@ -2063,21 +2246,37 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2063
2246
  const msg = mergeErr instanceof Error ? mergeErr.message : String(mergeErr);
2064
2247
  changesApplied = false;
2065
2248
  hadAnyChanges = false;
2249
+ mergePhaseFailed = true;
2250
+ for (const result of results) {
2251
+ if (result.producedChanges || result.recoveryRef || result.nestedPatches?.length) {
2252
+ setRecoveryAvailable(result);
2253
+ }
2254
+ }
2066
2255
  mergeSummary = `\n\n<system-notification>Merge phase failed: ${msg}\nTask outputs are preserved but changes were not applied.</system-notification>`;
2067
2256
  }
2068
2257
  }
2069
2258
 
2070
2259
  // Apply nested repo patches (separate from parent git)
2071
- if (isIsolated && repoRoot && (mergeMode === "branch" || changesApplied !== false)) {
2260
+ if (isIsolated && repoRoot && !mergePhaseFailed && (mergeMode === "branch" || changesApplied !== false)) {
2072
2261
  const allNestedPatches = results
2073
2262
  .filter(r => {
2074
- if (!r.nestedPatches || r.nestedPatches.length === 0 || r.exitCode !== 0 || r.aborted) {
2263
+ if (
2264
+ !r.nestedPatches ||
2265
+ r.nestedPatches.length === 0 ||
2266
+ r.exitCode !== 0 ||
2267
+ r.error ||
2268
+ r.aborted ||
2269
+ r.paused
2270
+ ) {
2075
2271
  return false;
2076
2272
  }
2077
2273
  if (mergeMode !== "branch") {
2078
2274
  return true;
2079
2275
  }
2080
- if (!r.branchName || !mergedBranchesForNestedPatches) {
2276
+ if (!r.branchName) {
2277
+ return true;
2278
+ }
2279
+ if (!mergedBranchesForNestedPatches) {
2081
2280
  return false;
2082
2281
  }
2083
2282
  return mergedBranchesForNestedPatches.has(r.branchName);
@@ -2085,32 +2284,46 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2085
2284
  .flatMap(r => r.nestedPatches!);
2086
2285
  if (allNestedPatches.length > 0) {
2087
2286
  try {
2088
- const commitMsg =
2089
- commitStyle === "ai" && this.session.modelRegistry
2090
- ? async (diff: string) => {
2091
- return generateCommitMessage(
2092
- diff,
2093
- this.session.modelRegistry!,
2094
- this.session.settings,
2095
- this.session.getSessionId?.() ?? undefined,
2096
- );
2097
- }
2098
- : undefined;
2099
- await applyNestedPatches(repoRoot, allNestedPatches, commitMsg);
2287
+ await applyNestedPatches(repoRoot, allNestedPatches);
2288
+ if (!baseline || !(await verifyNestedPatchesApplied(repoRoot, baseline, allNestedPatches))) {
2289
+ throw new Error("Nested repository persistence proof failed");
2290
+ }
2291
+ for (const result of results) {
2292
+ const rootApplied =
2293
+ mergeMode !== "branch"
2294
+ ? changesApplied !== false
2295
+ : !result.branchName || Boolean(mergedBranchesForNestedPatches?.has(result.branchName));
2296
+ if (
2297
+ result.nestedPatches?.length &&
2298
+ result.exitCode === 0 &&
2299
+ !result.error &&
2300
+ !result.aborted &&
2301
+ !result.paused &&
2302
+ rootApplied
2303
+ ) {
2304
+ result.persistence = {
2305
+ outcome: "applied",
2306
+ ownerWorktreeApplied: true,
2307
+ recoveryRef: result.recoveryRef,
2308
+ };
2309
+ }
2310
+ }
2100
2311
  } catch {
2101
- // Nested patch failures are non-fatal to the parent merge
2312
+ changesApplied = false;
2313
+ for (const result of results) {
2314
+ if (result.nestedPatches?.length) setRecoveryAvailable(result);
2315
+ }
2102
2316
  mergeSummary +=
2103
- "\n\n<system-notification>Some nested repository patches failed to apply.</system-notification>";
2317
+ "\n\n<system-notification>Some nested repository patches failed to apply; affected tasks remain recovery-only.</system-notification>";
2104
2318
  }
2105
2319
  }
2106
2320
  }
2107
2321
 
2108
2322
  // Build final output - match plugin format
2109
- const cancelledCount = results.filter(r => r.aborted).length;
2110
- const successCount = results.filter(r => r.exitCode === 0 && !r.error && !r.aborted).length;
2111
2323
  const totalDuration = Date.now() - startTime;
2112
-
2113
2324
  const receipts = results.map(buildTaskReceipt);
2325
+ const cancelledCount = receipts.filter(r => r.status === "aborted").length;
2326
+ const successCount = receipts.filter(r => r.status === "completed").length;
2114
2327
  const roiSummary = buildTaskRoiSummary(receipts);
2115
2328
  const roiReconciliation = executionOverrides?.suppressRoiReconciliation
2116
2329
  ? undefined
@@ -3,6 +3,7 @@ import type { AgentProgress, TaskToolDetails } from "./types";
3
3
  export type ProviderRetryKind = NonNullable<AgentProgress["retryState"]>["kind"];
4
4
 
5
5
  const FIRST_EVENT_TIMEOUT_PATTERN = /stream timed out while waiting for the first event/i;
6
+ const FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_MESSAGE = "Provider stream timed out while waiting for first event";
6
7
  const IDLE_STREAM_STALL_PATTERN = /stream stalled while waiting for the next event/i;
7
8
  const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
8
9
 
@@ -22,7 +23,9 @@ export function classifyProviderRetryFromTransport(facts: {
22
23
  return "first_event_timeout";
23
24
  }
24
25
  const errorMessage = facts.errorMessage ?? "";
25
- if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage)) return "first_event_timeout";
26
+ if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage) || errorMessage === FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_MESSAGE) {
27
+ return "first_event_timeout";
28
+ }
26
29
  if (IDLE_STREAM_STALL_PATTERN.test(errorMessage)) return "idle_stream_stall";
27
30
  return "provider_error";
28
31
  }
@@ -39,6 +39,7 @@ export interface TaskResultReceipt {
39
39
  cost?: number;
40
40
  usageCostBreakdownComplete?: true;
41
41
  branchName?: string;
42
+ persistence?: SingleResult["persistence"];
42
43
  retryFailure?: { attempt: number; errorSummary: string };
43
44
  setupFailure?: { summary: string };
44
45
  errorSummary?: string;
@@ -66,6 +67,9 @@ export interface TaskResultReceipt {
66
67
  forkContextAdvisory?: SingleResult["forkContextAdvisory"];
67
68
  /** Resolved repository identity for this delegated lane (#2901). */
68
69
  repositoryBinding?: SingleResult["repositoryBinding"];
70
+ /** Runtime-derived review source disposition and rerun guidance (#3469). */
71
+ reviewSource?: SingleResult["reviewSource"];
72
+
69
73
  roi?: TaskRoi;
70
74
  }
71
75
 
@@ -128,6 +132,18 @@ function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outp
128
132
  if (raw.retryFailure) {
129
133
  return `Task ${status}; retry stopped after attempt ${raw.retryFailure.attempt}.`;
130
134
  }
135
+ if (raw.persistence?.outcome === "recovery_available") {
136
+ const recovery = raw.persistence.recoveryRef
137
+ ? ` Recovery patch: ${raw.persistence.recoveryRef.uri} (${raw.persistence.recoveryRef.sizeBytes} bytes).`
138
+ : "";
139
+ return `Task ${status}; changes were not persisted to the owner worktree.${recovery}`;
140
+ }
141
+ if (raw.persistence?.outcome === "applied") {
142
+ return `Task ${status}; changes persisted to the owner worktree.`;
143
+ }
144
+ if (raw.persistence?.outcome === "no_changes") {
145
+ return `Task ${status}; no changes to persist.`;
146
+ }
131
147
  if (raw.abortReason) {
132
148
  return `Task ${status}; abort reason recorded.`;
133
149
  }
@@ -143,7 +159,7 @@ function buildSafeSynopsis(raw: SingleResult, outputRef: TaskResultReceipt["outp
143
159
  function getStatus(raw: SingleResult): TaskResultReceipt["status"] {
144
160
  if (raw.paused) return "paused";
145
161
  if (raw.aborted) return "aborted";
146
- if (raw.exitCode === 0 && raw.error) return "merge_failed";
162
+ if (raw.exitCode === 0 && (raw.error || raw.persistence?.outcome === "recovery_available")) return "merge_failed";
147
163
  if (raw.exitCode !== 0 || raw.error) return "failed";
148
164
  return "completed";
149
165
  }
@@ -275,11 +291,18 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
275
291
  usageCostBreakdownComplete:
276
292
  raw.usageCostBreakdownComplete === true && hasCompleteUsageCostBreakdown(raw.usage) ? true : undefined,
277
293
  branchName: raw.branchName,
294
+ persistence: raw.persistence,
278
295
  fastMode: raw.fastMode,
279
296
  retryFailure: raw.retryFailure
280
297
  ? { attempt: raw.retryFailure.attempt, errorSummary: "Retry failure recorded." }
281
298
  : undefined,
282
- errorSummary: raw.setupFailure?.summary ?? (raw.error ? "Error recorded." : undefined),
299
+ errorSummary:
300
+ raw.setupFailure?.summary ??
301
+ (raw.error
302
+ ? "Error recorded."
303
+ : raw.persistence?.outcome === "recovery_available"
304
+ ? "Changes were not persisted to the owner worktree."
305
+ : undefined),
283
306
  setupFailure: raw.setupFailure ? { summary: raw.setupFailure.summary } : undefined,
284
307
  abortSummary: raw.abortReason ? "Abort reason recorded." : undefined,
285
308
  preview,
@@ -291,6 +314,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
291
314
  forkContext: raw.forkContext,
292
315
  forkContextAdvisory: raw.forkContextAdvisory,
293
316
  repositoryBinding: raw.repositoryBinding,
317
+ reviewSource: raw.reviewSource,
294
318
  roi: buildTaskRoi(raw),
295
319
  };
296
320
  }