macro-agent 0.1.8 → 0.1.11

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 (306) hide show
  1. package/CLAUDE.md +263 -33
  2. package/README.md +781 -131
  3. package/dist/acp/claude-code-replay.d.ts +11 -0
  4. package/dist/acp/claude-code-replay.d.ts.map +1 -0
  5. package/dist/acp/claude-code-replay.js +190 -0
  6. package/dist/acp/claude-code-replay.js.map +1 -0
  7. package/dist/acp/macro-agent.d.ts.map +1 -1
  8. package/dist/acp/macro-agent.js +192 -7
  9. package/dist/acp/macro-agent.js.map +1 -1
  10. package/dist/acp/types.d.ts +9 -0
  11. package/dist/acp/types.d.ts.map +1 -1
  12. package/dist/acp/types.js.map +1 -1
  13. package/dist/adapters/tasks-adapter.d.ts.map +1 -1
  14. package/dist/adapters/tasks-adapter.js +3 -0
  15. package/dist/adapters/tasks-adapter.js.map +1 -1
  16. package/dist/adapters/types.d.ts +1 -0
  17. package/dist/adapters/types.d.ts.map +1 -1
  18. package/dist/agent/agent-manager-v2.d.ts +21 -0
  19. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  20. package/dist/agent/agent-manager-v2.js +308 -54
  21. package/dist/agent/agent-manager-v2.js.map +1 -1
  22. package/dist/agent/agent-manager.d.ts +12 -0
  23. package/dist/agent/agent-manager.d.ts.map +1 -1
  24. package/dist/agent/agent-manager.js.map +1 -1
  25. package/dist/agent/agent-store.d.ts +10 -0
  26. package/dist/agent/agent-store.d.ts.map +1 -1
  27. package/dist/agent/agent-store.js +22 -0
  28. package/dist/agent/agent-store.js.map +1 -1
  29. package/dist/agent/types.d.ts +15 -2
  30. package/dist/agent/types.d.ts.map +1 -1
  31. package/dist/agent/types.js.map +1 -1
  32. package/dist/boot-v2.d.ts +129 -1
  33. package/dist/boot-v2.d.ts.map +1 -1
  34. package/dist/boot-v2.js +359 -8
  35. package/dist/boot-v2.js.map +1 -1
  36. package/dist/cli/acp.js +4 -0
  37. package/dist/cli/acp.js.map +1 -1
  38. package/dist/cli/index.js +56 -0
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
  41. package/dist/cognitive/macro-agent-backend.js +40 -22
  42. package/dist/cognitive/macro-agent-backend.js.map +1 -1
  43. package/dist/integrations/skilltree.d.ts.map +1 -1
  44. package/dist/integrations/skilltree.js +1 -0
  45. package/dist/integrations/skilltree.js.map +1 -1
  46. package/dist/lifecycle/cascade.d.ts +25 -2
  47. package/dist/lifecycle/cascade.d.ts.map +1 -1
  48. package/dist/lifecycle/cascade.js +70 -2
  49. package/dist/lifecycle/cascade.js.map +1 -1
  50. package/dist/lifecycle/cleanup.d.ts +33 -2
  51. package/dist/lifecycle/cleanup.d.ts.map +1 -1
  52. package/dist/lifecycle/cleanup.js +28 -6
  53. package/dist/lifecycle/cleanup.js.map +1 -1
  54. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  55. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  56. package/dist/lifecycle/handlers-v2.js +28 -2
  57. package/dist/lifecycle/handlers-v2.js.map +1 -1
  58. package/dist/lifecycle/types.d.ts +11 -0
  59. package/dist/lifecycle/types.d.ts.map +1 -1
  60. package/dist/lifecycle/types.js.map +1 -1
  61. package/dist/map/acp-bridge.d.ts +9 -0
  62. package/dist/map/acp-bridge.d.ts.map +1 -1
  63. package/dist/map/acp-bridge.js +15 -2
  64. package/dist/map/acp-bridge.js.map +1 -1
  65. package/dist/map/cascade-action-handler.d.ts +24 -0
  66. package/dist/map/cascade-action-handler.d.ts.map +1 -0
  67. package/dist/map/cascade-action-handler.js +170 -0
  68. package/dist/map/cascade-action-handler.js.map +1 -0
  69. package/dist/map/cascade-bridge.d.ts +44 -0
  70. package/dist/map/cascade-bridge.d.ts.map +1 -0
  71. package/dist/map/cascade-bridge.js +294 -0
  72. package/dist/map/cascade-bridge.js.map +1 -0
  73. package/dist/map/coordination-handler.d.ts.map +1 -1
  74. package/dist/map/coordination-handler.js +12 -1
  75. package/dist/map/coordination-handler.js.map +1 -1
  76. package/dist/map/lifecycle-bridge.d.ts +1 -1
  77. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  78. package/dist/map/lifecycle-bridge.js +58 -23
  79. package/dist/map/lifecycle-bridge.js.map +1 -1
  80. package/dist/map/server.d.ts.map +1 -1
  81. package/dist/map/server.js +219 -7
  82. package/dist/map/server.js.map +1 -1
  83. package/dist/map/sidecar.d.ts.map +1 -1
  84. package/dist/map/sidecar.js +49 -2
  85. package/dist/map/sidecar.js.map +1 -1
  86. package/dist/map/types.d.ts +22 -0
  87. package/dist/map/types.d.ts.map +1 -1
  88. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  89. package/dist/mcp/tools/done-v2.js +8 -0
  90. package/dist/mcp/tools/done-v2.js.map +1 -1
  91. package/dist/teams/team-manager-v2.d.ts.map +1 -1
  92. package/dist/teams/team-manager-v2.js +26 -0
  93. package/dist/teams/team-manager-v2.js.map +1 -1
  94. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  95. package/dist/teams/team-runtime-v2.js +16 -3
  96. package/dist/teams/team-runtime-v2.js.map +1 -1
  97. package/dist/workspace/config.d.ts +10 -10
  98. package/dist/workspace/config.d.ts.map +1 -1
  99. package/dist/workspace/config.js +4 -4
  100. package/dist/workspace/config.js.map +1 -1
  101. package/dist/workspace/git-cascade-adapter.d.ts +510 -0
  102. package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
  103. package/dist/workspace/git-cascade-adapter.js +934 -0
  104. package/dist/workspace/git-cascade-adapter.js.map +1 -0
  105. package/dist/workspace/index.d.ts +3 -3
  106. package/dist/workspace/index.d.ts.map +1 -1
  107. package/dist/workspace/index.js +4 -4
  108. package/dist/workspace/index.js.map +1 -1
  109. package/dist/workspace/landing/direct-push.d.ts +20 -0
  110. package/dist/workspace/landing/direct-push.d.ts.map +1 -0
  111. package/dist/workspace/landing/direct-push.js +74 -0
  112. package/dist/workspace/landing/direct-push.js.map +1 -0
  113. package/dist/workspace/landing/index.d.ts +29 -0
  114. package/dist/workspace/landing/index.d.ts.map +1 -0
  115. package/dist/workspace/landing/index.js +37 -0
  116. package/dist/workspace/landing/index.js.map +1 -0
  117. package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
  118. package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
  119. package/dist/workspace/landing/merge-to-parent.js +186 -0
  120. package/dist/workspace/landing/merge-to-parent.js.map +1 -0
  121. package/dist/workspace/landing/optimistic-push.d.ts +16 -0
  122. package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
  123. package/dist/workspace/landing/optimistic-push.js +27 -0
  124. package/dist/workspace/landing/optimistic-push.js.map +1 -0
  125. package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
  126. package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
  127. package/dist/workspace/landing/queue-to-branch.js +79 -0
  128. package/dist/workspace/landing/queue-to-branch.js.map +1 -0
  129. package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
  130. package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
  131. package/dist/workspace/merge-queue/merge-queue.js +10 -0
  132. package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
  133. package/dist/workspace/merge-queue/types.d.ts +16 -2
  134. package/dist/workspace/merge-queue/types.d.ts.map +1 -1
  135. package/dist/workspace/merge-queue/types.js +9 -0
  136. package/dist/workspace/merge-queue/types.js.map +1 -1
  137. package/dist/workspace/pool/types.d.ts +1 -0
  138. package/dist/workspace/pool/types.d.ts.map +1 -1
  139. package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
  140. package/dist/workspace/pool/worktree-pool.js +1 -0
  141. package/dist/workspace/pool/worktree-pool.js.map +1 -1
  142. package/dist/workspace/recovery/abandon.d.ts +15 -0
  143. package/dist/workspace/recovery/abandon.d.ts.map +1 -0
  144. package/dist/workspace/recovery/abandon.js +45 -0
  145. package/dist/workspace/recovery/abandon.js.map +1 -0
  146. package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
  147. package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
  148. package/dist/workspace/recovery/auto-resolve.js +99 -0
  149. package/dist/workspace/recovery/auto-resolve.js.map +1 -0
  150. package/dist/workspace/recovery/defer.d.ts +15 -0
  151. package/dist/workspace/recovery/defer.d.ts.map +1 -0
  152. package/dist/workspace/recovery/defer.js +16 -0
  153. package/dist/workspace/recovery/defer.js.map +1 -0
  154. package/dist/workspace/recovery/escalate.d.ts +16 -0
  155. package/dist/workspace/recovery/escalate.d.ts.map +1 -0
  156. package/dist/workspace/recovery/escalate.js +24 -0
  157. package/dist/workspace/recovery/escalate.js.map +1 -0
  158. package/dist/workspace/recovery/index.d.ts +32 -0
  159. package/dist/workspace/recovery/index.d.ts.map +1 -0
  160. package/dist/workspace/recovery/index.js +45 -0
  161. package/dist/workspace/recovery/index.js.map +1 -0
  162. package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
  163. package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
  164. package/dist/workspace/recovery/spawn-resolver.js +118 -0
  165. package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
  166. package/dist/workspace/recovery/types.d.ts +63 -0
  167. package/dist/workspace/recovery/types.d.ts.map +1 -0
  168. package/dist/workspace/recovery/types.js +12 -0
  169. package/dist/workspace/recovery/types.js.map +1 -0
  170. package/dist/workspace/topology/index.d.ts +9 -0
  171. package/dist/workspace/topology/index.d.ts.map +1 -0
  172. package/dist/workspace/topology/index.js +8 -0
  173. package/dist/workspace/topology/index.js.map +1 -0
  174. package/dist/workspace/topology/no-workspace.d.ts +18 -0
  175. package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
  176. package/dist/workspace/topology/no-workspace.js +25 -0
  177. package/dist/workspace/topology/no-workspace.js.map +1 -0
  178. package/dist/workspace/topology/types.d.ts +97 -0
  179. package/dist/workspace/topology/types.d.ts.map +1 -0
  180. package/dist/workspace/topology/types.js +20 -0
  181. package/dist/workspace/topology/types.js.map +1 -0
  182. package/dist/workspace/topology/yaml-driven.d.ts +69 -0
  183. package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
  184. package/dist/workspace/topology/yaml-driven.js +273 -0
  185. package/dist/workspace/topology/yaml-driven.js.map +1 -0
  186. package/dist/workspace/types-v3.d.ts +117 -0
  187. package/dist/workspace/types-v3.d.ts.map +1 -0
  188. package/dist/workspace/types-v3.js +20 -0
  189. package/dist/workspace/types-v3.js.map +1 -0
  190. package/dist/workspace/types.d.ts +162 -17
  191. package/dist/workspace/types.d.ts.map +1 -1
  192. package/dist/workspace/workspace-manager.d.ts +101 -13
  193. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  194. package/dist/workspace/workspace-manager.js +416 -13
  195. package/dist/workspace/workspace-manager.js.map +1 -1
  196. package/dist/workspace/yaml-schema.d.ts +254 -0
  197. package/dist/workspace/yaml-schema.d.ts.map +1 -0
  198. package/dist/workspace/yaml-schema.js +170 -0
  199. package/dist/workspace/yaml-schema.js.map +1 -0
  200. package/docs/conflict-recovery.md +472 -0
  201. package/docs/design/task-dispatcher.md +880 -0
  202. package/docs/git-cascade-integration-gaps.md +678 -0
  203. package/docs/workspace-interfaces.md +731 -0
  204. package/docs/workspace-redesign-plan.md +302 -0
  205. package/package.json +6 -5
  206. package/src/__tests__/boot-v2.test.ts +435 -0
  207. package/src/__tests__/e2e/acp-over-map.e2e.test.ts +92 -0
  208. package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
  209. package/src/__tests__/e2e/bootstrap.e2e.test.ts +319 -0
  210. package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
  211. package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
  212. package/src/__tests__/e2e/dispatch-coordination.e2e.test.ts +495 -0
  213. package/src/__tests__/e2e/dispatch-live.e2e.test.ts +564 -0
  214. package/src/__tests__/e2e/dispatch-opentasks.e2e.test.ts +496 -0
  215. package/src/__tests__/e2e/dispatch-phase2-live.e2e.test.ts +456 -0
  216. package/src/__tests__/e2e/dispatch-phase2.e2e.test.ts +386 -0
  217. package/src/__tests__/e2e/dispatch.e2e.test.ts +376 -0
  218. package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
  219. package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
  220. package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
  221. package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
  222. package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
  223. package/src/acp/__tests__/macro-agent.test.ts +39 -1
  224. package/src/acp/claude-code-replay.ts +208 -0
  225. package/src/acp/macro-agent.ts +203 -10
  226. package/src/acp/types.ts +10 -0
  227. package/src/adapters/__tests__/tasks-adapter.test.ts +1 -0
  228. package/src/adapters/tasks-adapter.ts +3 -0
  229. package/src/adapters/types.ts +1 -0
  230. package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
  231. package/src/agent/__tests__/agent-manager-v2.test.ts +66 -0
  232. package/src/agent/__tests__/agent-store.test.ts +52 -0
  233. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  234. package/src/agent/agent-manager-v2.ts +372 -59
  235. package/src/agent/agent-manager.ts +14 -0
  236. package/src/agent/agent-store.ts +24 -0
  237. package/src/agent/types.ts +16 -2
  238. package/src/boot-v2.ts +589 -35
  239. package/src/cli/acp.ts +4 -0
  240. package/src/cli/index.ts +61 -0
  241. package/src/cognitive/macro-agent-backend.ts +45 -29
  242. package/src/integrations/skilltree.ts +1 -0
  243. package/src/lifecycle/__tests__/cascade-consolidation.test.ts +240 -0
  244. package/src/lifecycle/cascade.ts +77 -2
  245. package/src/lifecycle/cleanup.ts +52 -3
  246. package/src/lifecycle/handlers-v2.ts +40 -3
  247. package/src/lifecycle/types.ts +12 -0
  248. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  249. package/src/map/__tests__/emit-event.test.ts +71 -0
  250. package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
  251. package/src/map/acp-bridge.ts +26 -3
  252. package/src/map/cascade-action-handler.ts +205 -0
  253. package/src/map/cascade-bridge.ts +339 -0
  254. package/src/map/coordination-handler.ts +13 -1
  255. package/src/map/lifecycle-bridge.ts +52 -17
  256. package/src/map/server.ts +225 -7
  257. package/src/map/sidecar.ts +48 -1
  258. package/src/map/types.ts +23 -0
  259. package/src/mcp/tools/done-v2.ts +9 -0
  260. package/src/teams/team-manager-v2.ts +37 -0
  261. package/src/teams/team-runtime-v2.ts +23 -3
  262. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  263. package/src/workspace/__tests__/land-dispatch.test.ts +214 -0
  264. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  265. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  266. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  267. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  268. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  269. package/src/workspace/config.ts +11 -11
  270. package/src/workspace/git-cascade-adapter.ts +1213 -0
  271. package/src/workspace/index.ts +11 -11
  272. package/src/workspace/landing/__tests__/strategies.test.ts +184 -0
  273. package/src/workspace/landing/direct-push.ts +91 -0
  274. package/src/workspace/landing/index.ts +40 -0
  275. package/src/workspace/landing/merge-to-parent.ts +229 -0
  276. package/src/workspace/landing/optimistic-push.ts +36 -0
  277. package/src/workspace/landing/queue-to-branch.ts +108 -0
  278. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  279. package/src/workspace/merge-queue/types.ts +16 -2
  280. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  281. package/src/workspace/pool/types.ts +1 -0
  282. package/src/workspace/pool/worktree-pool.ts +1 -0
  283. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  284. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  285. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  286. package/src/workspace/recovery/abandon.ts +51 -0
  287. package/src/workspace/recovery/auto-resolve.ts +119 -0
  288. package/src/workspace/recovery/defer.ts +23 -0
  289. package/src/workspace/recovery/escalate.ts +30 -0
  290. package/src/workspace/recovery/index.ts +58 -0
  291. package/src/workspace/recovery/spawn-resolver.ts +152 -0
  292. package/src/workspace/recovery/types.ts +54 -0
  293. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  294. package/src/workspace/topology/index.ts +18 -0
  295. package/src/workspace/topology/no-workspace.ts +39 -0
  296. package/src/workspace/topology/types.ts +116 -0
  297. package/src/workspace/topology/yaml-driven.ts +316 -0
  298. package/src/workspace/types-v3.ts +162 -0
  299. package/src/workspace/types.ts +211 -20
  300. package/src/workspace/workspace-manager.ts +533 -19
  301. package/src/workspace/yaml-schema.ts +216 -0
  302. package/dist/workspace/dataplane-adapter.d.ts +0 -260
  303. package/dist/workspace/dataplane-adapter.d.ts.map +0 -1
  304. package/dist/workspace/dataplane-adapter.js +0 -416
  305. package/dist/workspace/dataplane-adapter.js.map +0 -1
  306. package/src/workspace/dataplane-adapter.ts +0 -546
package/src/cli/acp.ts CHANGED
@@ -34,6 +34,10 @@ async function main() {
34
34
  try {
35
35
  const system = await bootV2({
36
36
  cwd: defaultCwd,
37
+ // `--instance-id` picks the on-disk compartment under ~/.macro-agent/
38
+ // so multiple runs with the same id share state. Omit to auto-derive
39
+ // from the cwd hash. See `BootV2Config.instanceId`.
40
+ ...(options.instanceId ? { instanceId: options.instanceId } : {}),
37
41
  // Enable ACP WebSocket server if port is specified (server mode)
38
42
  ...(options.port
39
43
  ? {
package/src/cli/index.ts CHANGED
@@ -498,6 +498,67 @@ program
498
498
  }
499
499
  });
500
500
 
501
+ // ─────────────────────────────────────────────────────────────────
502
+ // Run Team Command (Phase 10)
503
+ // ─────────────────────────────────────────────────────────────────
504
+
505
+ program
506
+ .command("run <teamName>")
507
+ .description("Boot the system, start a team by name, and optionally prompt the root agent")
508
+ .option("--task <task>", "Task to prompt the root agent with")
509
+ .option("--cwd <path>", "Working directory for agents")
510
+ .option("--base-path <path>", "Base path for team YAML lookup (default: cwd)")
511
+ .action(async (teamName: string, options: { task?: string; cwd?: string; basePath?: string }) => {
512
+ const cwd = options.cwd ?? process.cwd();
513
+ console.log(chalk.blue(`Booting macro-agent and starting team: ${teamName}`));
514
+
515
+ let system: Awaited<ReturnType<typeof bootV2>> | null = null;
516
+ try {
517
+ system = await bootV2({ cwd });
518
+ console.log(chalk.green("System booted."));
519
+
520
+ // Construct & install a TeamManagerV2; start the requested team.
521
+ const { TeamManagerV2 } = await import("../teams/team-manager-v2.js");
522
+ const teamManager = new TeamManagerV2({
523
+ agentManager: system.agentManager,
524
+ inboxAdapter: system.inboxAdapter,
525
+ tasksAdapter: system.tasksAdapter,
526
+ });
527
+ teamManager.install();
528
+
529
+ const instanceId = await teamManager.startTeam(teamName, options.basePath ?? cwd);
530
+ console.log(chalk.green(`Team started: ${teamName} (instance ${instanceId})`));
531
+
532
+ // Find the root agent (first spawned agent in the instance)
533
+ const agents = system.agentStore.listAgents({ state: "running" });
534
+ const root = agents[0];
535
+
536
+ if (options.task && root) {
537
+ console.log(chalk.gray(`Prompting root agent (${root.id}) with task...`));
538
+ const iter = system.agentManager.prompt(root.id, options.task);
539
+ for await (const chunk of iter) {
540
+ process.stdout.write(typeof chunk === "string" ? chunk : JSON.stringify(chunk));
541
+ }
542
+ console.log();
543
+ } else if (!root) {
544
+ console.log(chalk.yellow("No root agent found to prompt."));
545
+ }
546
+
547
+ console.log(chalk.gray("Press Ctrl+C to shut down."));
548
+
549
+ // Graceful shutdown on SIGINT
550
+ process.on("SIGINT", async () => {
551
+ console.log(chalk.yellow("\nShutting down..."));
552
+ if (system) await system.shutdown();
553
+ process.exit(0);
554
+ });
555
+ } catch (error) {
556
+ console.error(chalk.red(`Failed to run team: ${error}`));
557
+ if (system) await system.shutdown();
558
+ process.exit(1);
559
+ }
560
+ });
561
+
501
562
  // ─────────────────────────────────────────────────────────────────
502
563
  // Parse and Run
503
564
  // ─────────────────────────────────────────────────────────────────
@@ -240,6 +240,46 @@ export class MacroAgentBackend {
240
240
  const timeout = config.timeout;
241
241
  let softTimer: ReturnType<typeof setTimeout> | undefined;
242
242
  let hardTimer: ReturnType<typeof setTimeout> | undefined;
243
+ let completionNotified = false;
244
+
245
+ // Helper: fire the completion callback + inbox notification exactly once.
246
+ // Called from both the hard-timer path and the finally block to avoid a
247
+ // race where waitForSession returns (state != "running") before finally
248
+ // runs and fires the callback.
249
+ const notifyCompletion = async (): Promise<void> => {
250
+ if (completionNotified) return;
251
+ if (session.state !== "completed" && session.state !== "failed") return;
252
+ completionNotified = true;
253
+
254
+ const completeEvent: SessionCompleteEvent = {
255
+ sessionId: session.id,
256
+ agentId: agentId as string,
257
+ state: session.state,
258
+ duration_ms: session.endTime
259
+ ? session.endTime.getTime() - session.startTime.getTime()
260
+ : 0,
261
+ message_count: session.messages.length,
262
+ tool_call_count: session.toolCalls.length,
263
+ };
264
+
265
+ this.onSessionComplete?.(completeEvent);
266
+
267
+ if (this.inboxAdapter) {
268
+ try {
269
+ await this.inboxAdapter.send(
270
+ agentId as string,
271
+ agentId as string,
272
+ {
273
+ type: "session.complete",
274
+ sessionId: session.id,
275
+ state: session.state,
276
+ duration_ms: completeEvent.duration_ms,
277
+ outcome: session.state === "completed" ? "success" : "failure",
278
+ },
279
+ );
280
+ } catch { /* best-effort */ }
281
+ }
282
+ };
243
283
 
244
284
  try {
245
285
  if (timeout && timeout > 0) {
@@ -263,6 +303,9 @@ export class MacroAgentBackend {
263
303
  session.state = "failed";
264
304
  session.error = "Timeout exceeded";
265
305
  session.endTime = new Date();
306
+ // Fire completion callback immediately — the finally block's
307
+ // deferred callback could race with waitForSession returning.
308
+ void notifyCompletion();
266
309
  this.agentManager.terminate(agentId, "timeout").catch(() => {});
267
310
  }, timeout);
268
311
  }
@@ -307,35 +350,8 @@ export class MacroAgentBackend {
307
350
  }
308
351
 
309
352
  // Notify completion (no trajectory extraction — OpenHive handles that via sessionlog)
310
- if (session.state === "completed" || session.state === "failed") {
311
- const completeEvent: SessionCompleteEvent = {
312
- sessionId: session.id,
313
- agentId: agentId as string,
314
- state: session.state,
315
- duration_ms: session.endTime
316
- ? session.endTime.getTime() - session.startTime.getTime()
317
- : 0,
318
- message_count: session.messages.length,
319
- tool_call_count: session.toolCalls.length,
320
- };
321
-
322
- this.onSessionComplete?.(completeEvent);
323
-
324
- if (this.inboxAdapter) {
325
- await this.inboxAdapter.send(
326
- agentId as string,
327
- agentId as string,
328
- {
329
- type: "session.complete",
330
- sessionId: session.id,
331
- state: session.state,
332
- duration_ms: completeEvent.duration_ms,
333
- outcome: session.state === "completed" ? "success" : "failure",
334
- },
335
- { subject: "session.complete" },
336
- ).catch(() => {});
337
- }
338
- }
353
+ // This is a no-op if the hard-timer path already fired it.
354
+ await notifyCompletion();
339
355
 
340
356
  // Clean up agent process
341
357
  if (session.state !== "running") {
@@ -55,6 +55,7 @@ async function loadSkillTree(): Promise<any> {
55
55
  _loadAttempted = true;
56
56
 
57
57
  try {
58
+ // @ts-ignore - optional peer dependency, may not be installed
58
59
  _skillTreeModule = await import("skill-tree");
59
60
  return _skillTreeModule;
60
61
  } catch {
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Consolidation-through-tracker tests (A3 follow-up).
3
+ *
4
+ * Verifies `terminateWithChangeConsolidation` prefers
5
+ * `workspaceManager.mergeStream()` when both workspaces carry stream ids —
6
+ * so the merge fires `x-cascade/stream.merged` and propagates to the hub —
7
+ * and cleanly falls back to raw `git merge` when streams aren't available.
8
+ *
9
+ * Unit-only: stubs workspaces + agent manager. No git invocations.
10
+ */
11
+
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
13
+ import { terminateWithChangeConsolidation } from '../cascade.js';
14
+ import type {
15
+ CascadeAgentManager,
16
+ WorkspaceProvider,
17
+ } from '../cascade.js';
18
+ import type { Workspace, WorkspaceManager } from '../../workspace/types.js';
19
+ import * as cleanupModule from '../cleanup.js';
20
+
21
+ function mkWorkspace(overrides: Partial<Workspace> = {}): Workspace {
22
+ return {
23
+ agentId: 'agent',
24
+ path: '/tmp/wt',
25
+ branch: 'stream/s',
26
+ streamId: 's',
27
+ role: 'worker' as any,
28
+ createdAt: Date.now(),
29
+ ...overrides,
30
+ };
31
+ }
32
+
33
+ function mkAgentManager(): CascadeAgentManager {
34
+ return {
35
+ getChildren: vi.fn(() => []),
36
+ terminate: vi.fn(async () => {}),
37
+ };
38
+ }
39
+
40
+ describe('terminateWithChangeConsolidation', () => {
41
+ let attemptMergeSpy: ReturnType<typeof vi.spyOn>;
42
+ let getCurrentBranchSpy: ReturnType<typeof vi.spyOn>;
43
+
44
+ beforeEach(() => {
45
+ // Default: branch matches so the "parent on expected branch" warning doesn't fire.
46
+ getCurrentBranchSpy = vi
47
+ .spyOn(cleanupModule, 'getCurrentBranch')
48
+ .mockReturnValue('stream/parent');
49
+ attemptMergeSpy = vi.spyOn(cleanupModule, 'attemptMerge');
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.restoreAllMocks();
54
+ });
55
+
56
+ it('routes through workspaceManager.mergeStream when both workspaces have stream ids', async () => {
57
+ const mergeStream = vi.fn(() => ({ success: true, newHead: 'abc123' }));
58
+ const ws = { mergeStream } as unknown as WorkspaceManager;
59
+ const provider: WorkspaceProvider = {
60
+ getWorkspace: (id) =>
61
+ id === 'child'
62
+ ? mkWorkspace({ agentId: 'child', streamId: 'child-s', branch: 'stream/child' })
63
+ : mkWorkspace({ agentId: 'parent', streamId: 'parent-s', branch: 'stream/parent', path: '/tmp/parent' }),
64
+ };
65
+ const am = mkAgentManager();
66
+
67
+ const result = await terminateWithChangeConsolidation(
68
+ 'child' as any,
69
+ 'parent' as any,
70
+ am,
71
+ provider,
72
+ undefined,
73
+ ws,
74
+ );
75
+
76
+ expect(result).toEqual({ success: true, merged: true, mergeCommit: 'abc123' });
77
+ expect(mergeStream).toHaveBeenCalledWith({
78
+ sourceStreamId: 'child-s',
79
+ targetStreamId: 'parent-s',
80
+ agentId: 'parent',
81
+ worktree: '/tmp/parent',
82
+ metadata: undefined,
83
+ });
84
+ // Raw-git path must NOT be touched when the tracker handled it.
85
+ expect(attemptMergeSpy).not.toHaveBeenCalled();
86
+ expect(am.terminate).toHaveBeenCalledWith('child', 'changes_consolidated');
87
+ });
88
+
89
+ it('threads a provided taskRef into the mergeStream metadata', async () => {
90
+ const mergeStream = vi.fn(() => ({ success: true, newHead: 'def456' }));
91
+ const ws = { mergeStream } as unknown as WorkspaceManager;
92
+ const provider: WorkspaceProvider = {
93
+ getWorkspace: (id) =>
94
+ mkWorkspace({
95
+ agentId: id,
96
+ streamId: `${id}-s`,
97
+ branch: `stream/${id}`,
98
+ }),
99
+ };
100
+
101
+ const taskRef = { resource_id: 'res-a1', node_id: 'task-a1' };
102
+ await terminateWithChangeConsolidation(
103
+ 'child' as any,
104
+ 'parent' as any,
105
+ mkAgentManager(),
106
+ provider,
107
+ undefined,
108
+ ws,
109
+ taskRef,
110
+ );
111
+
112
+ expect(mergeStream).toHaveBeenCalledWith(
113
+ expect.objectContaining({ metadata: { task_ref: taskRef } }),
114
+ );
115
+ });
116
+
117
+ it('surfaces tracker conflicts without falling back to raw git', async () => {
118
+ const mergeStream = vi.fn(() => ({
119
+ success: false,
120
+ conflicts: ['shared.ts'],
121
+ }));
122
+ const ws = { mergeStream } as unknown as WorkspaceManager;
123
+ const provider: WorkspaceProvider = {
124
+ getWorkspace: (id) =>
125
+ mkWorkspace({
126
+ agentId: id,
127
+ streamId: `${id}-s`,
128
+ branch: `stream/${id}`,
129
+ }),
130
+ };
131
+ const am = mkAgentManager();
132
+
133
+ const result = await terminateWithChangeConsolidation(
134
+ 'child' as any,
135
+ 'parent' as any,
136
+ am,
137
+ provider,
138
+ undefined,
139
+ ws,
140
+ );
141
+
142
+ expect(result.success).toBe(false);
143
+ expect(result.conflicts).toEqual(['shared.ts']);
144
+ expect(attemptMergeSpy).not.toHaveBeenCalled();
145
+ expect(am.terminate).toHaveBeenCalledWith('child', 'merge_conflict');
146
+ });
147
+
148
+ it('falls back to raw git when workspaceManager is not provided', async () => {
149
+ attemptMergeSpy.mockReturnValue({
150
+ success: true,
151
+ mergeCommit: 'rawabc',
152
+ });
153
+ const provider: WorkspaceProvider = {
154
+ getWorkspace: (id) =>
155
+ mkWorkspace({
156
+ agentId: id,
157
+ streamId: `${id}-s`,
158
+ branch: `stream/${id}`,
159
+ }),
160
+ };
161
+ const am = mkAgentManager();
162
+
163
+ const result = await terminateWithChangeConsolidation(
164
+ 'child' as any,
165
+ 'parent' as any,
166
+ am,
167
+ provider,
168
+ undefined,
169
+ // No workspaceManager argument.
170
+ );
171
+
172
+ expect(result).toEqual({ success: true, merged: true, mergeCommit: 'rawabc' });
173
+ expect(attemptMergeSpy).toHaveBeenCalledOnce();
174
+ });
175
+
176
+ it('falls back to raw git when the child workspace lacks a stream id', async () => {
177
+ attemptMergeSpy.mockReturnValue({ success: true, mergeCommit: 'legacyabc' });
178
+ const mergeStream = vi.fn();
179
+ const ws = { mergeStream } as unknown as WorkspaceManager;
180
+ const provider: WorkspaceProvider = {
181
+ getWorkspace: (id) =>
182
+ id === 'child'
183
+ ? mkWorkspace({ agentId: 'child', streamId: '', branch: 'feat/legacy' })
184
+ : mkWorkspace({ agentId: 'parent', streamId: 'parent-s', branch: 'stream/parent' }),
185
+ };
186
+
187
+ const result = await terminateWithChangeConsolidation(
188
+ 'child' as any,
189
+ 'parent' as any,
190
+ mkAgentManager(),
191
+ provider,
192
+ undefined,
193
+ ws,
194
+ );
195
+
196
+ expect(result.success).toBe(true);
197
+ expect(mergeStream).not.toHaveBeenCalled();
198
+ expect(attemptMergeSpy).toHaveBeenCalledOnce();
199
+ });
200
+
201
+ it('falls back to raw git when tracker.mergeStream throws', async () => {
202
+ const mergeStream = vi.fn(() => {
203
+ throw new Error('stream not found');
204
+ });
205
+ attemptMergeSpy.mockReturnValue({ success: true, mergeCommit: 'fallbackabc' });
206
+ const ws = { mergeStream } as unknown as WorkspaceManager;
207
+ const provider: WorkspaceProvider = {
208
+ getWorkspace: (id) =>
209
+ mkWorkspace({
210
+ agentId: id,
211
+ streamId: `${id}-s`,
212
+ branch: `stream/${id}`,
213
+ }),
214
+ };
215
+
216
+ const result = await terminateWithChangeConsolidation(
217
+ 'child' as any,
218
+ 'parent' as any,
219
+ mkAgentManager(),
220
+ provider,
221
+ undefined,
222
+ ws,
223
+ );
224
+
225
+ expect(result.success).toBe(true);
226
+ expect(mergeStream).toHaveBeenCalledOnce();
227
+ expect(attemptMergeSpy).toHaveBeenCalledOnce();
228
+ });
229
+
230
+ it('preserves legacy "no workspace provider" behavior', async () => {
231
+ const am = mkAgentManager();
232
+ const result = await terminateWithChangeConsolidation(
233
+ 'child' as any,
234
+ 'parent' as any,
235
+ am,
236
+ );
237
+ expect(result).toEqual({ success: true, merged: false });
238
+ expect(am.terminate).toHaveBeenCalledWith('child', 'parent_stopped');
239
+ });
240
+ });
@@ -129,11 +129,24 @@ export interface WorkspaceProvider {
129
129
  * If a merge conflict occurs, the merge is aborted and the child is
130
130
  * terminated with a "merge_conflict" reason.
131
131
  *
132
+ * When a `workspaceManager` is provided AND both workspaces carry stream
133
+ * ids, the merge is routed through `workspaceManager.mergeStream()` so
134
+ * it flows through git-cascade's tracker — gaining a `stream.merged`
135
+ * cascade event that propagates to the hub via CascadeBridge. This is
136
+ * the primary observability win of A3 in the integration plan.
137
+ *
138
+ * Falls back to raw `attemptMerge` (unchanged behavior) when either
139
+ * workspace lacks a stream id or no manager is passed — so
140
+ * programmatic/legacy callers keep working unchanged.
141
+ *
132
142
  * @param childId - Child agent to terminate
133
143
  * @param parentId - Parent agent to consolidate changes into
134
144
  * @param agentManager - Agent manager for operations
135
145
  * @param workspaceProvider - Optional workspace provider for getting agent workspaces
136
146
  * @param options - Optional consolidation options
147
+ * @param workspaceManager - Optional workspace manager; enables cascade-
148
+ * event emission on the consolidation merge when both workspaces are
149
+ * stream-backed.
137
150
  * @returns ConsolidationResult indicating success or failure
138
151
  */
139
152
  export async function terminateWithChangeConsolidation(
@@ -141,7 +154,15 @@ export async function terminateWithChangeConsolidation(
141
154
  parentId: AgentId,
142
155
  agentManager: CascadeAgentManager,
143
156
  workspaceProvider?: WorkspaceProvider,
144
- options?: ConsolidationOptions
157
+ options?: ConsolidationOptions,
158
+ workspaceManager?: WorkspaceManager,
159
+ /**
160
+ * Optional task reference inherited from the parent agent's metadata.
161
+ * When provided + routing through the tracker, threaded into the
162
+ * `x-cascade/stream.merged` emit so the hub's cascade_merges row records
163
+ * which task drove the consolidation.
164
+ */
165
+ taskRef?: { resource_id: string; node_id: string }
145
166
  ): Promise<ConsolidationResult> {
146
167
  // If no workspace provider, just terminate normally
147
168
  if (!workspaceProvider) {
@@ -171,7 +192,61 @@ export async function terminateWithChangeConsolidation(
171
192
  // Continue with merge anyway - use the actual current branch
172
193
  }
173
194
 
174
- // Attempt to merge child branch into parent's worktree
195
+ // Attempt to merge child branch into parent's worktree.
196
+ //
197
+ // Preferred path: route through workspaceManager.mergeStream when both
198
+ // workspaces have stream ids. Goes via git-cascade's tracker.mergeStream,
199
+ // which fires `x-cascade/stream.merged` with source/target stream ids so
200
+ // the hub's cascade projection + the source stream's `merged` status
201
+ // update in lockstep with the actual git operation.
202
+ if (
203
+ workspaceManager &&
204
+ childWorkspace.streamId &&
205
+ parentWorkspace.streamId
206
+ ) {
207
+ try {
208
+ const result = workspaceManager.mergeStream({
209
+ sourceStreamId: childWorkspace.streamId,
210
+ targetStreamId: parentWorkspace.streamId,
211
+ agentId: parentId,
212
+ worktree: parentWorkspace.path,
213
+ metadata: taskRef ? { task_ref: taskRef } : undefined,
214
+ });
215
+ if (result.success) {
216
+ await agentManager.terminate(childId, "changes_consolidated");
217
+ return {
218
+ success: true,
219
+ merged: true,
220
+ mergeCommit: result.newHead,
221
+ };
222
+ }
223
+ if (result.conflicts && result.conflicts.length > 0) {
224
+ // Tracker already aborted; emit and terminate with conflict reason.
225
+ console.warn(
226
+ `[cascade] Merge conflict consolidating ${childId} -> ${parentId} via tracker: ${result.conflicts.join(", ")}`
227
+ );
228
+ await agentManager.terminate(childId, "merge_conflict");
229
+ return {
230
+ success: false,
231
+ merged: false,
232
+ conflicts: result.conflicts,
233
+ };
234
+ }
235
+ // Tracker returned a non-conflict failure — fall through to raw
236
+ // git path so we don't regress the consolidation semantics for
237
+ // cases the tracker can't handle (e.g., stream in unexpected
238
+ // status, detached HEAD, branch-name mismatches).
239
+ } catch (err) {
240
+ console.warn(
241
+ `[cascade] tracker.mergeStream failed consolidating ${childId} -> ${parentId}; falling back to raw git: ${err instanceof Error ? err.message : String(err)}`
242
+ );
243
+ // Fall through to raw-git path below.
244
+ }
245
+ }
246
+
247
+ // Fallback: raw git merge. Preserves pre-A3 behavior when streams
248
+ // aren't available (programmatic callers, legacy role-shaped
249
+ // workspaces) or when the tracker path bailed.
175
250
  const mergeMessage =
176
251
  options?.mergeMessage ??
177
252
  `Merge changes from ${childId} (${childBranch})`;
@@ -124,15 +124,45 @@ export function detectCleanupStatus(
124
124
  // =============================================================================
125
125
 
126
126
  /**
127
- * Commit all uncommitted changes in a workspace
127
+ * Optional handle for routing commits through git-cascade so the resulting
128
+ * commit gets a Change-Id trailer and emits an `x-cascade/stream.committed`
129
+ * event. When omitted, commits are made via raw git (no Change-Id, no
130
+ * cascade event) — used by legacy/null-workspace paths.
131
+ */
132
+ export interface TrackedCommitHandle {
133
+ /** WorkspaceManager-style commitChanges signature */
134
+ commitChanges(opts: {
135
+ streamId: string;
136
+ agentId: string;
137
+ worktree: string;
138
+ message: string;
139
+ metadata?: Record<string, unknown>;
140
+ }): { commit: string; changeId: string };
141
+ /** Stream this commit belongs to */
142
+ streamId: string;
143
+ /** Agent making the commit */
144
+ agentId: string;
145
+ /** Optional metadata threaded into the cascade event (e.g. `{ task_ref }`) */
146
+ metadata?: Record<string, unknown>;
147
+ }
148
+
149
+ /**
150
+ * Commit all uncommitted changes in a workspace.
151
+ *
152
+ * When a `TrackedCommitHandle` is supplied, the commit goes through
153
+ * git-cascade's tracker — gaining a stable Change-Id trailer and emitting
154
+ * `x-cascade/stream.committed` so OpenHive sees the work. Without a handle,
155
+ * falls back to raw git (legacy behavior).
128
156
  *
129
157
  * @param workspacePath - Path to the workspace
130
158
  * @param message - Commit message
159
+ * @param tracked - Optional handle to commit via the cascade tracker
131
160
  * @returns Commit hash if successful, undefined if nothing to commit
132
161
  */
133
162
  export function commitChanges(
134
163
  workspacePath: string,
135
- message: string
164
+ message: string,
165
+ tracked?: TrackedCommitHandle
136
166
  ): string | undefined {
137
167
  try {
138
168
  // Check if there are changes to commit
@@ -140,7 +170,26 @@ export function commitChanges(
140
170
  return undefined;
141
171
  }
142
172
 
143
- // Stage all changes
173
+ // Tracked path: stage + commit through git-cascade so Change-Id +
174
+ // x-cascade events fire.
175
+ if (tracked) {
176
+ try {
177
+ const { commit } = tracked.commitChanges({
178
+ streamId: tracked.streamId,
179
+ agentId: tracked.agentId,
180
+ worktree: workspacePath,
181
+ message,
182
+ metadata: tracked.metadata,
183
+ });
184
+ return commit;
185
+ } catch {
186
+ // Fall through to raw-git path on tracker failure (e.g., stream
187
+ // conflicted) so callers still get a commit. Caveat: no Change-Id
188
+ // and no cascade event in this fallback.
189
+ }
190
+ }
191
+
192
+ // Raw-git path: stage all changes
144
193
  execFileSync("git", ["add", "--all"], {
145
194
  cwd: workspacePath,
146
195
  encoding: "utf-8",
@@ -22,8 +22,9 @@ import type {
22
22
  CleanupStatus,
23
23
  DoneHandlerResult,
24
24
  } from "./types.js";
25
- import { commitChanges, attemptMerge, abortMerge } from "./cleanup.js";
25
+ import { commitChanges, attemptMerge, abortMerge, type TrackedCommitHandle } from "./cleanup.js";
26
26
  import type { MergeQueueInterface } from "../workspace/merge-queue/types.js";
27
+ import type { WorkspaceManager } from "../workspace/types.js";
27
28
  import {
28
29
  getAllDescendants,
29
30
  needsCascadeTermination,
@@ -40,6 +41,40 @@ export interface HandlerDepsV2 {
40
41
  agentManager: AgentManager;
41
42
  taskMode?: "push" | "pull";
42
43
  mergeQueue?: MergeQueueInterface;
44
+ /**
45
+ * Optional workspace manager. When provided AND `context.streamId` is set,
46
+ * commits route through the cascade tracker (Change-Id + x-cascade events).
47
+ * Without it, commits use raw git (legacy / null-workspace path).
48
+ */
49
+ workspaceManager?: WorkspaceManager;
50
+ }
51
+
52
+ // =============================================================================
53
+ // Tracked Commit Helper
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Build a TrackedCommitHandle when the agent has a streamId + workspaceManager.
58
+ * Returns undefined if either is missing — caller falls back to raw git.
59
+ */
60
+ function buildTrackedHandle(
61
+ context: LifecycleContext,
62
+ deps: HandlerDepsV2
63
+ ): TrackedCommitHandle | undefined {
64
+ if (!context.streamId || !deps.workspaceManager) return undefined;
65
+ const ws = deps.workspaceManager;
66
+ // Build metadata with task_ref if known. Empty object is fine — the hub
67
+ // ignores unknown fields, and back-fill kicks in if task_ref appears later.
68
+ const metadata: Record<string, unknown> = {};
69
+ if (context.taskRef) {
70
+ metadata.task_ref = context.taskRef;
71
+ }
72
+ return {
73
+ streamId: context.streamId,
74
+ agentId: context.agentId,
75
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
76
+ commitChanges: (opts) => ws.commitChanges(opts),
77
+ };
43
78
  }
44
79
 
45
80
  // =============================================================================
@@ -110,10 +145,12 @@ async function handleWorkerDone(
110
145
  ? `WIP: ${args.summary}`
111
146
  : `WIP: Auto-commit from done() with ${uncommittedCount} uncommitted file(s)`;
112
147
 
113
- const commitHash = commitChanges(context.workspacePath, commitMessage);
148
+ const tracked = buildTrackedHandle(context, deps);
149
+ const commitHash = commitChanges(context.workspacePath, commitMessage, tracked);
114
150
  if (commitHash) {
151
+ const via = tracked ? "tracker" : "raw-git";
115
152
  cleanupActions.push(
116
- `Committed ${uncommittedCount} file(s): ${commitHash.slice(0, 8)}`
153
+ `Committed ${uncommittedCount} file(s) via ${via}: ${commitHash.slice(0, 8)}`
117
154
  );
118
155
  } else {
119
156
  warnings.push("Failed to auto-commit uncommitted changes");