macro-agent 0.1.7 → 0.1.10

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 (259) hide show
  1. package/CLAUDE.md +179 -38
  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 +155 -6
  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/agent/agent-manager-v2.d.ts +21 -0
  14. package/dist/agent/agent-manager-v2.d.ts.map +1 -1
  15. package/dist/agent/agent-manager-v2.js +234 -71
  16. package/dist/agent/agent-manager-v2.js.map +1 -1
  17. package/dist/agent/agent-manager.d.ts +12 -0
  18. package/dist/agent/agent-manager.d.ts.map +1 -1
  19. package/dist/agent/agent-manager.js.map +1 -1
  20. package/dist/agent/types.d.ts +15 -2
  21. package/dist/agent/types.d.ts.map +1 -1
  22. package/dist/agent/types.js.map +1 -1
  23. package/dist/boot-v2.d.ts +41 -0
  24. package/dist/boot-v2.d.ts.map +1 -1
  25. package/dist/boot-v2.js +34 -37
  26. package/dist/boot-v2.js.map +1 -1
  27. package/dist/cli/index.js +56 -0
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
  30. package/dist/cognitive/macro-agent-backend.js +40 -22
  31. package/dist/cognitive/macro-agent-backend.js.map +1 -1
  32. package/dist/integrations/skilltree.d.ts.map +1 -1
  33. package/dist/integrations/skilltree.js +1 -0
  34. package/dist/integrations/skilltree.js.map +1 -1
  35. package/dist/lifecycle/cleanup.d.ts +33 -2
  36. package/dist/lifecycle/cleanup.d.ts.map +1 -1
  37. package/dist/lifecycle/cleanup.js +28 -6
  38. package/dist/lifecycle/cleanup.js.map +1 -1
  39. package/dist/lifecycle/handlers-v2.d.ts +7 -0
  40. package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
  41. package/dist/lifecycle/handlers-v2.js +28 -2
  42. package/dist/lifecycle/handlers-v2.js.map +1 -1
  43. package/dist/lifecycle/types.d.ts +11 -0
  44. package/dist/lifecycle/types.d.ts.map +1 -1
  45. package/dist/lifecycle/types.js.map +1 -1
  46. package/dist/map/acp-bridge.d.ts +9 -0
  47. package/dist/map/acp-bridge.d.ts.map +1 -1
  48. package/dist/map/acp-bridge.js +15 -2
  49. package/dist/map/acp-bridge.js.map +1 -1
  50. package/dist/map/cascade-bridge.d.ts +44 -0
  51. package/dist/map/cascade-bridge.d.ts.map +1 -0
  52. package/dist/map/cascade-bridge.js +257 -0
  53. package/dist/map/cascade-bridge.js.map +1 -0
  54. package/dist/map/lifecycle-bridge.d.ts +1 -8
  55. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  56. package/dist/map/lifecycle-bridge.js +76 -22
  57. package/dist/map/lifecycle-bridge.js.map +1 -1
  58. package/dist/map/server.d.ts.map +1 -1
  59. package/dist/map/server.js +47 -6
  60. package/dist/map/server.js.map +1 -1
  61. package/dist/map/sidecar.d.ts.map +1 -1
  62. package/dist/map/sidecar.js +33 -4
  63. package/dist/map/sidecar.js.map +1 -1
  64. package/dist/map/types.d.ts +20 -0
  65. package/dist/map/types.d.ts.map +1 -1
  66. package/dist/mcp/tools/done-v2.d.ts.map +1 -1
  67. package/dist/mcp/tools/done-v2.js +8 -0
  68. package/dist/mcp/tools/done-v2.js.map +1 -1
  69. package/dist/teams/team-manager-v2.d.ts.map +1 -1
  70. package/dist/teams/team-manager-v2.js +26 -0
  71. package/dist/teams/team-manager-v2.js.map +1 -1
  72. package/dist/teams/team-runtime-v2.d.ts.map +1 -1
  73. package/dist/teams/team-runtime-v2.js +16 -3
  74. package/dist/teams/team-runtime-v2.js.map +1 -1
  75. package/dist/workspace/config.d.ts +10 -10
  76. package/dist/workspace/config.d.ts.map +1 -1
  77. package/dist/workspace/config.js +4 -4
  78. package/dist/workspace/config.js.map +1 -1
  79. package/dist/workspace/git-cascade-adapter.d.ts +510 -0
  80. package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
  81. package/dist/workspace/git-cascade-adapter.js +908 -0
  82. package/dist/workspace/git-cascade-adapter.js.map +1 -0
  83. package/dist/workspace/index.d.ts +3 -3
  84. package/dist/workspace/index.d.ts.map +1 -1
  85. package/dist/workspace/index.js +4 -4
  86. package/dist/workspace/index.js.map +1 -1
  87. package/dist/workspace/landing/direct-push.d.ts +20 -0
  88. package/dist/workspace/landing/direct-push.d.ts.map +1 -0
  89. package/dist/workspace/landing/direct-push.js +74 -0
  90. package/dist/workspace/landing/direct-push.js.map +1 -0
  91. package/dist/workspace/landing/index.d.ts +29 -0
  92. package/dist/workspace/landing/index.d.ts.map +1 -0
  93. package/dist/workspace/landing/index.js +37 -0
  94. package/dist/workspace/landing/index.js.map +1 -0
  95. package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
  96. package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
  97. package/dist/workspace/landing/merge-to-parent.js +185 -0
  98. package/dist/workspace/landing/merge-to-parent.js.map +1 -0
  99. package/dist/workspace/landing/optimistic-push.d.ts +16 -0
  100. package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
  101. package/dist/workspace/landing/optimistic-push.js +27 -0
  102. package/dist/workspace/landing/optimistic-push.js.map +1 -0
  103. package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
  104. package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
  105. package/dist/workspace/landing/queue-to-branch.js +79 -0
  106. package/dist/workspace/landing/queue-to-branch.js.map +1 -0
  107. package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
  108. package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
  109. package/dist/workspace/merge-queue/merge-queue.js +10 -0
  110. package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
  111. package/dist/workspace/merge-queue/types.d.ts +16 -2
  112. package/dist/workspace/merge-queue/types.d.ts.map +1 -1
  113. package/dist/workspace/merge-queue/types.js +9 -0
  114. package/dist/workspace/merge-queue/types.js.map +1 -1
  115. package/dist/workspace/pool/types.d.ts +1 -0
  116. package/dist/workspace/pool/types.d.ts.map +1 -1
  117. package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
  118. package/dist/workspace/pool/worktree-pool.js +1 -0
  119. package/dist/workspace/pool/worktree-pool.js.map +1 -1
  120. package/dist/workspace/recovery/abandon.d.ts +15 -0
  121. package/dist/workspace/recovery/abandon.d.ts.map +1 -0
  122. package/dist/workspace/recovery/abandon.js +45 -0
  123. package/dist/workspace/recovery/abandon.js.map +1 -0
  124. package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
  125. package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
  126. package/dist/workspace/recovery/auto-resolve.js +99 -0
  127. package/dist/workspace/recovery/auto-resolve.js.map +1 -0
  128. package/dist/workspace/recovery/defer.d.ts +15 -0
  129. package/dist/workspace/recovery/defer.d.ts.map +1 -0
  130. package/dist/workspace/recovery/defer.js +16 -0
  131. package/dist/workspace/recovery/defer.js.map +1 -0
  132. package/dist/workspace/recovery/escalate.d.ts +16 -0
  133. package/dist/workspace/recovery/escalate.d.ts.map +1 -0
  134. package/dist/workspace/recovery/escalate.js +24 -0
  135. package/dist/workspace/recovery/escalate.js.map +1 -0
  136. package/dist/workspace/recovery/index.d.ts +32 -0
  137. package/dist/workspace/recovery/index.d.ts.map +1 -0
  138. package/dist/workspace/recovery/index.js +45 -0
  139. package/dist/workspace/recovery/index.js.map +1 -0
  140. package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
  141. package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
  142. package/dist/workspace/recovery/spawn-resolver.js +111 -0
  143. package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
  144. package/dist/workspace/recovery/types.d.ts +63 -0
  145. package/dist/workspace/recovery/types.d.ts.map +1 -0
  146. package/dist/workspace/recovery/types.js +12 -0
  147. package/dist/workspace/recovery/types.js.map +1 -0
  148. package/dist/workspace/topology/index.d.ts +9 -0
  149. package/dist/workspace/topology/index.d.ts.map +1 -0
  150. package/dist/workspace/topology/index.js +8 -0
  151. package/dist/workspace/topology/index.js.map +1 -0
  152. package/dist/workspace/topology/no-workspace.d.ts +18 -0
  153. package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
  154. package/dist/workspace/topology/no-workspace.js +25 -0
  155. package/dist/workspace/topology/no-workspace.js.map +1 -0
  156. package/dist/workspace/topology/types.d.ts +97 -0
  157. package/dist/workspace/topology/types.d.ts.map +1 -0
  158. package/dist/workspace/topology/types.js +20 -0
  159. package/dist/workspace/topology/types.js.map +1 -0
  160. package/dist/workspace/topology/yaml-driven.d.ts +69 -0
  161. package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
  162. package/dist/workspace/topology/yaml-driven.js +273 -0
  163. package/dist/workspace/topology/yaml-driven.js.map +1 -0
  164. package/dist/workspace/types-v3.d.ts +110 -0
  165. package/dist/workspace/types-v3.d.ts.map +1 -0
  166. package/dist/workspace/types-v3.js +20 -0
  167. package/dist/workspace/types-v3.js.map +1 -0
  168. package/dist/workspace/types.d.ts +145 -17
  169. package/dist/workspace/types.d.ts.map +1 -1
  170. package/dist/workspace/workspace-manager.d.ts +92 -13
  171. package/dist/workspace/workspace-manager.d.ts.map +1 -1
  172. package/dist/workspace/workspace-manager.js +373 -13
  173. package/dist/workspace/workspace-manager.js.map +1 -1
  174. package/dist/workspace/yaml-schema.d.ts +254 -0
  175. package/dist/workspace/yaml-schema.d.ts.map +1 -0
  176. package/dist/workspace/yaml-schema.js +170 -0
  177. package/dist/workspace/yaml-schema.js.map +1 -0
  178. package/docs/conflict-recovery.md +472 -0
  179. package/docs/git-cascade-integration-gaps.md +678 -0
  180. package/docs/workspace-interfaces.md +731 -0
  181. package/docs/workspace-redesign-plan.md +302 -0
  182. package/package.json +4 -4
  183. package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
  184. package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
  185. package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
  186. package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
  187. package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
  188. package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
  189. package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
  190. package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
  191. package/src/acp/__tests__/macro-agent.test.ts +39 -1
  192. package/src/acp/claude-code-replay.ts +208 -0
  193. package/src/acp/macro-agent.ts +167 -9
  194. package/src/acp/types.ts +10 -0
  195. package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
  196. package/src/agent/__tests__/agent-manager-v2.test.ts +71 -11
  197. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  198. package/src/agent/agent-manager-v2.ts +293 -77
  199. package/src/agent/agent-manager.ts +14 -0
  200. package/src/agent/types.ts +16 -2
  201. package/src/boot-v2.ts +87 -36
  202. package/src/cli/index.ts +61 -0
  203. package/src/cognitive/__tests__/macro-agent-backend.test.ts +47 -5
  204. package/src/cognitive/macro-agent-backend.ts +45 -29
  205. package/src/integrations/skilltree.ts +1 -0
  206. package/src/lifecycle/cleanup.ts +52 -3
  207. package/src/lifecycle/handlers-v2.ts +40 -3
  208. package/src/lifecycle/types.ts +12 -0
  209. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  210. package/src/map/__tests__/lifecycle-bridge.test.ts +165 -22
  211. package/src/map/acp-bridge.ts +26 -3
  212. package/src/map/cascade-bridge.ts +301 -0
  213. package/src/map/lifecycle-bridge.ts +77 -27
  214. package/src/map/server.ts +47 -6
  215. package/src/map/sidecar.ts +31 -3
  216. package/src/map/types.ts +20 -0
  217. package/src/mcp/tools/done-v2.ts +9 -0
  218. package/src/teams/team-manager-v2.ts +37 -0
  219. package/src/teams/team-runtime-v2.ts +23 -3
  220. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  221. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  222. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  223. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  224. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  225. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  226. package/src/workspace/config.ts +11 -11
  227. package/src/workspace/git-cascade-adapter.ts +1186 -0
  228. package/src/workspace/index.ts +11 -11
  229. package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
  230. package/src/workspace/landing/direct-push.ts +91 -0
  231. package/src/workspace/landing/index.ts +40 -0
  232. package/src/workspace/landing/merge-to-parent.ts +228 -0
  233. package/src/workspace/landing/optimistic-push.ts +36 -0
  234. package/src/workspace/landing/queue-to-branch.ts +108 -0
  235. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  236. package/src/workspace/merge-queue/types.ts +16 -2
  237. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  238. package/src/workspace/pool/types.ts +1 -0
  239. package/src/workspace/pool/worktree-pool.ts +1 -0
  240. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  241. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  242. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  243. package/src/workspace/recovery/abandon.ts +51 -0
  244. package/src/workspace/recovery/auto-resolve.ts +119 -0
  245. package/src/workspace/recovery/defer.ts +23 -0
  246. package/src/workspace/recovery/escalate.ts +30 -0
  247. package/src/workspace/recovery/index.ts +58 -0
  248. package/src/workspace/recovery/spawn-resolver.ts +145 -0
  249. package/src/workspace/recovery/types.ts +54 -0
  250. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  251. package/src/workspace/topology/index.ts +18 -0
  252. package/src/workspace/topology/no-workspace.ts +39 -0
  253. package/src/workspace/topology/types.ts +116 -0
  254. package/src/workspace/topology/yaml-driven.ts +316 -0
  255. package/src/workspace/types-v3.ts +155 -0
  256. package/src/workspace/types.ts +191 -20
  257. package/src/workspace/workspace-manager.ts +474 -19
  258. package/src/workspace/yaml-schema.ts +216 -0
  259. package/src/workspace/dataplane-adapter.ts +0 -546
@@ -4,6 +4,13 @@
4
4
  * Coordinates merging of parallel worker branches into the integration branch.
5
5
  * Workers submit completed work; Integrator processes sequentially.
6
6
  *
7
+ * @deprecated Since v3 workspace redesign. Duplicates git-cascade's built-in
8
+ * `mergeQueue` module. New code uses `GitCascadeAdapter.addToMergeQueue`
9
+ * via `LandingStrategy` ("queue-to-branch"). Kept to preserve the legacy
10
+ * role-name dispatch path until teams migrate to `macro_agent.workspace`
11
+ * YAML. Scheduled for removal; see `docs/workspace-redesign-plan.md`
12
+ * Phases 6/8/9.
13
+ *
7
14
  * @module workspace/merge-queue/merge-queue
8
15
  * @implements [[s-bcqm]] Merge Queue section
9
16
  */
@@ -99,6 +106,9 @@ export interface MergeQueueConfig {
99
106
  * MergeQueue implementation.
100
107
  *
101
108
  * Manages merge requests for coordinating parallel worker merges.
109
+ *
110
+ * @deprecated Use git-cascade's built-in queue via
111
+ * `GitCascadeAdapter.addToMergeQueue` / `getNextToMerge`. See module doc.
102
112
  */
103
113
  export class MergeQueue implements MergeQueueInterface {
104
114
  private readonly db: Database.Database;
@@ -3,6 +3,15 @@
3
3
  *
4
4
  * Types for the merge queue layer that coordinates parallel worker merges.
5
5
  *
6
+ * @deprecated Since v3 workspace redesign. This module duplicates git-cascade's
7
+ * built-in `mergeQueue` (see `GitCascadeAdapter.addToMergeQueue`,
8
+ * `getNextToMerge`, etc.). New code should use `LandingStrategy`
9
+ * ("queue-to-branch") + git-cascade's queue directly. Kept in place to
10
+ * preserve the legacy role-name dispatch path (coordinator/worker/integrator)
11
+ * until teams migrate to `macro_agent.workspace` YAML. Scheduled for
12
+ * removal once self-driving is migrated. See `docs/workspace-redesign-plan.md`
13
+ * Phases 6/8/9.
14
+ *
6
15
  * @module workspace/merge-queue/types
7
16
  * @implements [[s-bcqm]] Merge Queue Schema section
8
17
  */
@@ -36,7 +45,7 @@ export interface MergeRequest {
36
45
  /** Stream (integration branch) this MR targets */
37
46
  streamId: string;
38
47
 
39
- /** Dataplane task ID this MR completes */
48
+ /** git-cascade task ID this MR completes */
40
49
  taskId: string;
41
50
 
42
51
  /** Git branch containing the worker's changes */
@@ -83,7 +92,7 @@ export interface SubmitMergeRequestOptions {
83
92
  /** Stream (integration branch) to merge into */
84
93
  streamId: string;
85
94
 
86
- /** Dataplane task ID this completes */
95
+ /** git-cascade task ID this completes */
87
96
  taskId: string;
88
97
 
89
98
  /** Git branch containing the worker's changes */
@@ -141,6 +150,11 @@ export type MergeQueueEventCallback = (event: MergeQueueEvent) => void;
141
150
  * Coordinates merging of parallel worker branches into the integration branch.
142
151
  * Workers submit completed work; Integrator processes sequentially.
143
152
  */
153
+ /**
154
+ * @deprecated Use git-cascade's built-in merge queue via
155
+ * `GitCascadeAdapter.addToMergeQueue` / `getNextToMerge` etc. See module
156
+ * doc for migration guidance.
157
+ */
144
158
  export interface MergeQueueInterface {
145
159
  // ─────────────────────────────────────────────────────────────────────────────
146
160
  // Submit
@@ -18,7 +18,7 @@ import { execSync } from 'child_process';
18
18
  import Database from 'better-sqlite3';
19
19
  import { WorktreePool } from '../worktree-pool.js';
20
20
  import { createWorkspaceManager, DefaultWorkspaceManager } from '../../workspace-manager.js';
21
- import { createDataplaneAdapter, DataplaneAdapter } from '../../dataplane-adapter.js';
21
+ import { createGitCascadeAdapter, GitCascadeAdapter } from '../../git-cascade-adapter.js';
22
22
  import type { PoolEvent, PoolStats } from '../types.js';
23
23
  import type { WorkerWorkspace, IntegratorWorkspace, CoordinatorWorkspace } from '../../types.js';
24
24
 
@@ -694,12 +694,12 @@ describe('WorktreePool Integration', () => {
694
694
 
695
695
  describe('WorkspaceManager integration', () => {
696
696
  let db: Database.Database;
697
- let adapter: DataplaneAdapter;
697
+ let adapter: GitCascadeAdapter;
698
698
  let manager: DefaultWorkspaceManager;
699
699
 
700
700
  beforeEach(() => {
701
701
  db = new Database(dbPath);
702
- adapter = createDataplaneAdapter({
702
+ adapter = createGitCascadeAdapter({
703
703
  enabled: true,
704
704
  repoPath,
705
705
  db,
@@ -944,12 +944,12 @@ describe('WorktreePool Integration', () => {
944
944
 
945
945
  describe('full lifecycle with pool', () => {
946
946
  let db: Database.Database;
947
- let adapter: DataplaneAdapter;
947
+ let adapter: GitCascadeAdapter;
948
948
  let manager: DefaultWorkspaceManager;
949
949
 
950
950
  beforeEach(() => {
951
951
  db = new Database(dbPath);
952
- adapter = createDataplaneAdapter({
952
+ adapter = createGitCascadeAdapter({
953
953
  enabled: true,
954
954
  repoPath,
955
955
  db,
@@ -147,6 +147,7 @@ export interface PoolStats {
147
147
  worker: number;
148
148
  integrator: number;
149
149
  coordinator: number;
150
+ v3: number;
150
151
  };
151
152
  }
152
153
 
@@ -484,6 +484,7 @@ export class WorktreePool implements WorktreePoolInterface {
484
484
  worker: 0,
485
485
  integrator: 0,
486
486
  coordinator: 0,
487
+ v3: 0,
487
488
  },
488
489
  };
489
490
 
@@ -0,0 +1,127 @@
1
+ /**
2
+ * AutoResolveStrategy integration tests.
3
+ *
4
+ * Creates a real merge conflict in a temp git repo and verifies the strategy
5
+ * replays the merge with `ours`/`theirs` and commits the resolution.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import * as os from 'os';
12
+ import { execSync } from 'child_process';
13
+ import { AutoResolveStrategy } from '../auto-resolve.js';
14
+ import type { WorkspaceManager } from '../../types.js';
15
+ import type { ConflictContext } from '../types.js';
16
+
17
+ function mockWorkspaceManager(): WorkspaceManager {
18
+ return {
19
+ resolveConflict: vi.fn(),
20
+ } as unknown as WorkspaceManager;
21
+ }
22
+
23
+ function sh(cmd: string, cwd: string): string {
24
+ return execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8' }).trim();
25
+ }
26
+
27
+ describe('AutoResolveStrategy (real git)', () => {
28
+ let tempDir: string;
29
+ let repoPath: string;
30
+
31
+ beforeEach(() => {
32
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'auto-resolve-'));
33
+ repoPath = path.join(tempDir, 'repo');
34
+ fs.mkdirSync(repoPath);
35
+
36
+ sh('git init -b main', repoPath);
37
+ sh('git config user.email "t@t.com"', repoPath);
38
+ sh('git config user.name "T"', repoPath);
39
+
40
+ // Base commit
41
+ fs.writeFileSync(path.join(repoPath, 'f.txt'), 'base\n');
42
+ sh('git add .', repoPath);
43
+ sh('git commit -m "base"', repoPath);
44
+
45
+ // Branch A (main) with change
46
+ fs.writeFileSync(path.join(repoPath, 'f.txt'), 'main-change\n');
47
+ sh('git add .', repoPath);
48
+ sh('git commit -m "main change"', repoPath);
49
+
50
+ // Branch B with conflicting change
51
+ sh('git checkout -b feature HEAD~1', repoPath);
52
+ fs.writeFileSync(path.join(repoPath, 'f.txt'), 'feature-change\n');
53
+ sh('git add .', repoPath);
54
+ sh('git commit -m "feature change"', repoPath);
55
+
56
+ // Checkout main + attempt merge (conflicts)
57
+ sh('git checkout main', repoPath);
58
+ try {
59
+ sh('git merge feature --no-edit', repoPath);
60
+ } catch {
61
+ // Expected — creates the conflict state
62
+ }
63
+ // Abort so AutoResolve starts from a clean state; it will re-trigger
64
+ // the merge via `git merge -X <strategy>`.
65
+ sh('git merge --abort', repoPath);
66
+ });
67
+
68
+ afterEach(() => {
69
+ if (fs.existsSync(tempDir)) {
70
+ fs.rmSync(tempDir, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ it('resolves with -X ours — keeps main-change', async () => {
75
+ const strat = new AutoResolveStrategy();
76
+ const ws = mockWorkspaceManager();
77
+
78
+ const ctx: ConflictContext = {
79
+ conflictId: 'c-test',
80
+ streamId: 's-1',
81
+ paths: ['f.txt'],
82
+ operation: 'merge',
83
+ worktree: repoPath,
84
+ sourceCommit: 'feature',
85
+ recoveryDepth: 0,
86
+ strategyConfig: { strategy: 'ours' },
87
+ workspaceManager: ws,
88
+ };
89
+
90
+ const result = await strat.recover(ctx);
91
+
92
+ expect(result.kind).toBe('resolved');
93
+ if (result.kind === 'resolved') {
94
+ expect(result.resolutionCommit).toMatch(/^[0-9a-f]+$/);
95
+ }
96
+
97
+ // Verify file has the 'ours' version
98
+ expect(fs.readFileSync(path.join(repoPath, 'f.txt'), 'utf-8')).toBe('main-change\n');
99
+
100
+ // WorkspaceManager.resolveConflict was notified
101
+ expect(ws.resolveConflict).toHaveBeenCalledWith(
102
+ expect.objectContaining({ conflictId: 'c-test' })
103
+ );
104
+ });
105
+
106
+ it('resolves with -X theirs — keeps feature-change', async () => {
107
+ const strat = new AutoResolveStrategy();
108
+ const ws = mockWorkspaceManager();
109
+
110
+ const ctx: ConflictContext = {
111
+ conflictId: 'c-test',
112
+ streamId: 's-1',
113
+ paths: ['f.txt'],
114
+ operation: 'merge',
115
+ worktree: repoPath,
116
+ sourceCommit: 'feature',
117
+ recoveryDepth: 0,
118
+ strategyConfig: { strategy: 'theirs' },
119
+ workspaceManager: ws,
120
+ };
121
+
122
+ const result = await strat.recover(ctx);
123
+
124
+ expect(result.kind).toBe('resolved');
125
+ expect(fs.readFileSync(path.join(repoPath, 'f.txt'), 'utf-8')).toBe('feature-change\n');
126
+ });
127
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * SpawnResolverStrategy tests (Phase 7b).
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import {
7
+ SpawnResolverStrategy,
8
+ createSpawnResolverStrategy,
9
+ } from '../spawn-resolver.js';
10
+ import type { ConflictContext } from '../types.js';
11
+ import type { WorkspaceManager } from '../../types.js';
12
+ import type { AgentManager } from '../../../agent/agent-manager.js';
13
+
14
+ type EventListener = (event: { type: string; data: Record<string, unknown> }) => void;
15
+
16
+ function mockManagers(): {
17
+ ws: WorkspaceManager;
18
+ am: AgentManager;
19
+ triggerResolved: (conflictId: string, commit: string) => void;
20
+ } {
21
+ const listeners = new Set<EventListener>();
22
+
23
+ const ws = {
24
+ onEvent: vi.fn((cb: EventListener) => {
25
+ listeners.add(cb);
26
+ return () => listeners.delete(cb);
27
+ }),
28
+ } as unknown as WorkspaceManager;
29
+
30
+ const am = {
31
+ spawn: vi.fn().mockResolvedValue({ id: 'resolver-agent-1' }),
32
+ } as unknown as AgentManager;
33
+
34
+ const triggerResolved = (conflictId: string, commit: string) => {
35
+ for (const listener of listeners) {
36
+ listener({
37
+ type: 'conflict:resolved',
38
+ data: { conflictId, resolutionCommit: commit },
39
+ });
40
+ }
41
+ };
42
+
43
+ return { ws, am, triggerResolved };
44
+ }
45
+
46
+ function mockContext(ws: WorkspaceManager, overrides: Partial<ConflictContext> = {}): ConflictContext {
47
+ return {
48
+ conflictId: 'c-1',
49
+ streamId: 'stream-1',
50
+ paths: ['a.ts'],
51
+ operation: 'merge',
52
+ recoveryDepth: 0,
53
+ workspaceManager: ws,
54
+ ...overrides,
55
+ };
56
+ }
57
+
58
+ describe('SpawnResolverStrategy', () => {
59
+ it('spawns a resolver with the configured role and awaits resolve', async () => {
60
+ const { ws, am, triggerResolved } = mockManagers();
61
+ const strat = createSpawnResolverStrategy({ agentManager: am });
62
+
63
+ const ctx = mockContext(ws, {
64
+ strategyConfig: { role: 'resolver', timeout_ms: 5000 },
65
+ });
66
+
67
+ const recoveryPromise = strat.recover(ctx);
68
+
69
+ // Allow spawn + onEvent registration to complete
70
+ await new Promise((r) => setTimeout(r, 10));
71
+
72
+ expect(am.spawn).toHaveBeenCalledWith(
73
+ expect.objectContaining({
74
+ role: 'resolver',
75
+ capabilities: expect.arrayContaining(['workspace.resolve']),
76
+ })
77
+ );
78
+
79
+ // Simulate resolver agent calling resolve_conflict
80
+ triggerResolved('c-1', 'abc123');
81
+
82
+ const resolution = await recoveryPromise;
83
+ expect(resolution.kind).toBe('resolved');
84
+ if (resolution.kind === 'resolved') {
85
+ expect(resolution.resolutionCommit).toBe('abc123');
86
+ }
87
+ });
88
+
89
+ it('escalates on timeout', async () => {
90
+ const { ws, am } = mockManagers();
91
+ const strat = createSpawnResolverStrategy({
92
+ agentManager: am,
93
+ defaultTimeoutMs: 50, // short timeout for test
94
+ });
95
+
96
+ const ctx = mockContext(ws);
97
+ const resolution = await strat.recover(ctx);
98
+
99
+ expect(resolution.kind).toBe('escalated');
100
+ if (resolution.kind === 'escalated') {
101
+ expect(resolution.escalatedTo).toBe('human');
102
+ }
103
+ });
104
+
105
+ it('returns retry-after when max_concurrent exceeded', async () => {
106
+ const { ws, am, triggerResolved } = mockManagers();
107
+ const strat = createSpawnResolverStrategy({
108
+ agentManager: am,
109
+ defaultMaxConcurrent: 1,
110
+ defaultTimeoutMs: 5000,
111
+ });
112
+
113
+ // First resolver is still pending
114
+ const first = strat.recover(mockContext(ws));
115
+ await new Promise((r) => setTimeout(r, 10));
116
+
117
+ // Second try — exceeds max
118
+ const second = await strat.recover(mockContext(ws));
119
+ expect(second.kind).toBe('retry-after');
120
+
121
+ // Resolve the first so it cleans up
122
+ triggerResolved('c-1', 'commit');
123
+ await first;
124
+ });
125
+
126
+ it('returns failed when spawn itself fails', async () => {
127
+ const { ws } = mockManagers();
128
+ const am = {
129
+ spawn: vi.fn().mockRejectedValue(new Error('spawn boom')),
130
+ } as unknown as AgentManager;
131
+ const strat = new SpawnResolverStrategy({ agentManager: am });
132
+
133
+ const resolution = await strat.recover(mockContext(ws));
134
+ expect(resolution.kind).toBe('failed');
135
+ if (resolution.kind === 'failed') {
136
+ expect(resolution.error).toMatch(/spawn boom/);
137
+ }
138
+ });
139
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Conflict recovery strategy tests (Phase 7).
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import {
7
+ DeferStrategy,
8
+ AbandonStrategy,
9
+ EscalateStrategy,
10
+ AutoResolveStrategy,
11
+ buildBuiltinRecoveryRegistry,
12
+ } from '../index.js';
13
+ import type { ConflictContext } from '../types.js';
14
+ import type { WorkspaceManager } from '../../types.js';
15
+
16
+ function mockContext(overrides: Partial<ConflictContext> = {}): ConflictContext {
17
+ const ws = {
18
+ abandonStream: vi.fn(),
19
+ pauseStream: vi.fn(),
20
+ } as unknown as WorkspaceManager;
21
+ return {
22
+ conflictId: 'c-1',
23
+ streamId: 'stream-1',
24
+ paths: ['a.ts'],
25
+ operation: 'merge',
26
+ recoveryDepth: 0,
27
+ workspaceManager: ws,
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ describe('conflict recovery strategies', () => {
33
+ describe('DeferStrategy', () => {
34
+ it('returns { kind: deferred }', async () => {
35
+ const strat = new DeferStrategy();
36
+ const res = await strat.recover(mockContext());
37
+ expect(res.kind).toBe('deferred');
38
+ });
39
+ });
40
+
41
+ describe('AbandonStrategy', () => {
42
+ it('abandons the stream and returns { kind: abandoned }', async () => {
43
+ const strat = new AbandonStrategy();
44
+ const ctx = mockContext();
45
+ const res = await strat.recover(ctx);
46
+ expect(res.kind).toBe('abandoned');
47
+ expect(ctx.workspaceManager.abandonStream).toHaveBeenCalledWith(
48
+ 'stream-1',
49
+ expect.objectContaining({ reason: expect.stringContaining('c-1') })
50
+ );
51
+ });
52
+
53
+ it('returns { kind: failed } on abandon error', async () => {
54
+ const strat = new AbandonStrategy();
55
+ const ctx = mockContext();
56
+ (ctx.workspaceManager.abandonStream as any) = vi.fn(() => {
57
+ throw new Error('boom');
58
+ });
59
+ const res = await strat.recover(ctx);
60
+ expect(res.kind).toBe('failed');
61
+ if (res.kind === 'failed') {
62
+ expect(res.error).toContain('boom');
63
+ }
64
+ });
65
+ });
66
+
67
+ describe('EscalateStrategy', () => {
68
+ it('pauses the stream and returns { kind: escalated }', async () => {
69
+ const strat = new EscalateStrategy();
70
+ const ctx = mockContext();
71
+ const res = await strat.recover(ctx);
72
+ expect(res.kind).toBe('escalated');
73
+ expect(ctx.workspaceManager.pauseStream).toHaveBeenCalledWith(
74
+ 'stream-1',
75
+ expect.any(String)
76
+ );
77
+ });
78
+
79
+ it('uses notify config when provided', async () => {
80
+ const strat = new EscalateStrategy();
81
+ const ctx = mockContext({
82
+ strategyConfig: { notify: 'team:alpha' },
83
+ });
84
+ const res = await strat.recover(ctx);
85
+ expect(res.kind).toBe('escalated');
86
+ if (res.kind === 'escalated') {
87
+ expect(res.escalatedTo).toBe('team:alpha');
88
+ }
89
+ });
90
+ });
91
+
92
+ describe('AutoResolveStrategy', () => {
93
+ it('canHandle requires merge operation + worktree', () => {
94
+ const strat = new AutoResolveStrategy();
95
+ expect(
96
+ strat.canHandle!(mockContext({ operation: 'merge', worktree: '/tmp/wt' }))
97
+ ).toBe(true);
98
+ expect(strat.canHandle!(mockContext({ operation: 'merge' }))).toBe(false);
99
+ expect(
100
+ strat.canHandle!(mockContext({ operation: 'rebase', worktree: '/tmp/wt' }))
101
+ ).toBe(false);
102
+ });
103
+
104
+ it('returns failed for non-merge operations', async () => {
105
+ const strat = new AutoResolveStrategy();
106
+ const res = await strat.recover(mockContext({ operation: 'rebase' }));
107
+ expect(res.kind).toBe('failed');
108
+ });
109
+
110
+ it('returns failed without worktree', async () => {
111
+ const strat = new AutoResolveStrategy();
112
+ const res = await strat.recover(mockContext({ operation: 'merge' }));
113
+ expect(res.kind).toBe('failed');
114
+ if (res.kind === 'failed') {
115
+ expect(res.error).toMatch(/worktree/);
116
+ }
117
+ });
118
+
119
+ it('rejects unsupported strategies', async () => {
120
+ const strat = new AutoResolveStrategy();
121
+ const res = await strat.recover(
122
+ mockContext({
123
+ operation: 'merge',
124
+ worktree: '/tmp/wt',
125
+ strategyConfig: { strategy: 'bogus' },
126
+ })
127
+ );
128
+ expect(res.kind).toBe('failed');
129
+ if (res.kind === 'failed') {
130
+ expect(res.error).toMatch(/unsupported strategy/);
131
+ }
132
+ });
133
+ });
134
+
135
+ describe('buildBuiltinRecoveryRegistry', () => {
136
+ it('includes 4 built-in strategies', () => {
137
+ const registry = buildBuiltinRecoveryRegistry();
138
+ expect(registry.has('defer')).toBe(true);
139
+ expect(registry.has('abandon')).toBe(true);
140
+ expect(registry.has('escalate')).toBe(true);
141
+ expect(registry.has('auto-resolve')).toBe(true);
142
+ expect(registry.has('spawn-resolver')).toBe(false); // Phase 7b
143
+ });
144
+ });
145
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `abandon` conflict recovery strategy.
3
+ *
4
+ * Abandons the conflicted stream. Throwaway-exploration teams; CI-driven
5
+ * flows where broken work is discarded rather than resolved.
6
+ *
7
+ * @module workspace/recovery/abandon
8
+ */
9
+
10
+ import type {
11
+ ConflictContext,
12
+ ConflictRecoveryStrategy,
13
+ ConflictResolution,
14
+ } from './types.js';
15
+
16
+ export class AbandonStrategy implements ConflictRecoveryStrategy {
17
+ readonly name = 'abandon';
18
+ readonly mode = 'sync' as const;
19
+
20
+ async recover(ctx: ConflictContext): Promise<ConflictResolution> {
21
+ try {
22
+ // Mark the conflict resolved (method='abandoned') so the OpenHive hub
23
+ // moves cascade_conflicts.status from pending → resolved instead of
24
+ // showing it stuck pending forever.
25
+ try {
26
+ ctx.workspaceManager.resolveConflict({
27
+ conflictId: ctx.conflictId,
28
+ resolvedBy: 'system:abandon',
29
+ method: 'abandoned',
30
+ summary: `stream abandoned: ${ctx.streamId}`,
31
+ });
32
+ } catch {
33
+ // Non-fatal — abandonStream below still runs
34
+ }
35
+
36
+ ctx.workspaceManager.abandonStream(ctx.streamId, {
37
+ reason: `abandon strategy: conflict ${ctx.conflictId}`,
38
+ });
39
+ return {
40
+ kind: 'abandoned',
41
+ streamId: ctx.streamId,
42
+ reason: `conflict ${ctx.conflictId}`,
43
+ };
44
+ } catch (err) {
45
+ return {
46
+ kind: 'failed',
47
+ error: err instanceof Error ? err.message : String(err),
48
+ };
49
+ }
50
+ }
51
+ }