macro-agent 0.1.8 → 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 (258) hide show
  1. package/CLAUDE.md +166 -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 +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 -43
  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 +16 -1
  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 -1
  55. package/dist/map/lifecycle-bridge.d.ts.map +1 -1
  56. package/dist/map/lifecycle-bridge.js +58 -23
  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 -2
  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 +66 -0
  197. package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
  198. package/src/agent/agent-manager-v2.ts +293 -48
  199. package/src/agent/agent-manager.ts +14 -0
  200. package/src/agent/types.ts +16 -2
  201. package/src/boot-v2.ts +68 -1
  202. package/src/cli/index.ts +61 -0
  203. package/src/cognitive/macro-agent-backend.ts +45 -29
  204. package/src/integrations/skilltree.ts +1 -0
  205. package/src/lifecycle/cleanup.ts +52 -3
  206. package/src/lifecycle/handlers-v2.ts +40 -3
  207. package/src/lifecycle/types.ts +12 -0
  208. package/src/map/__tests__/cascade-bridge.test.ts +229 -0
  209. package/src/map/__tests__/lifecycle-bridge.test.ts +86 -10
  210. package/src/map/acp-bridge.ts +26 -3
  211. package/src/map/cascade-bridge.ts +301 -0
  212. package/src/map/lifecycle-bridge.ts +52 -17
  213. package/src/map/server.ts +47 -6
  214. package/src/map/sidecar.ts +31 -1
  215. package/src/map/types.ts +20 -0
  216. package/src/mcp/tools/done-v2.ts +9 -0
  217. package/src/teams/team-manager-v2.ts +37 -0
  218. package/src/teams/team-runtime-v2.ts +23 -3
  219. package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
  220. package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
  221. package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
  222. package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
  223. package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
  224. package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
  225. package/src/workspace/config.ts +11 -11
  226. package/src/workspace/git-cascade-adapter.ts +1186 -0
  227. package/src/workspace/index.ts +11 -11
  228. package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
  229. package/src/workspace/landing/direct-push.ts +91 -0
  230. package/src/workspace/landing/index.ts +40 -0
  231. package/src/workspace/landing/merge-to-parent.ts +228 -0
  232. package/src/workspace/landing/optimistic-push.ts +36 -0
  233. package/src/workspace/landing/queue-to-branch.ts +108 -0
  234. package/src/workspace/merge-queue/merge-queue.ts +10 -0
  235. package/src/workspace/merge-queue/types.ts +16 -2
  236. package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
  237. package/src/workspace/pool/types.ts +1 -0
  238. package/src/workspace/pool/worktree-pool.ts +1 -0
  239. package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
  240. package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
  241. package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
  242. package/src/workspace/recovery/abandon.ts +51 -0
  243. package/src/workspace/recovery/auto-resolve.ts +119 -0
  244. package/src/workspace/recovery/defer.ts +23 -0
  245. package/src/workspace/recovery/escalate.ts +30 -0
  246. package/src/workspace/recovery/index.ts +58 -0
  247. package/src/workspace/recovery/spawn-resolver.ts +145 -0
  248. package/src/workspace/recovery/types.ts +54 -0
  249. package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
  250. package/src/workspace/topology/index.ts +18 -0
  251. package/src/workspace/topology/no-workspace.ts +39 -0
  252. package/src/workspace/topology/types.ts +116 -0
  253. package/src/workspace/topology/yaml-driven.ts +316 -0
  254. package/src/workspace/types-v3.ts +155 -0
  255. package/src/workspace/types.ts +191 -20
  256. package/src/workspace/workspace-manager.ts +474 -19
  257. package/src/workspace/yaml-schema.ts +216 -0
  258. package/src/workspace/dataplane-adapter.ts +0 -546
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Workspace Module
3
3
  *
4
- * Provides workspace isolation for agents using dataplane for git management.
4
+ * Provides workspace isolation for agents using git-cascade for stream and worktree management.
5
5
  * Implements [[s-7ktd]] Structured Workspace Isolation.
6
6
  *
7
7
  * @module workspace
@@ -9,23 +9,23 @@
9
9
 
10
10
  // Configuration types
11
11
  export {
12
- type DataplaneConfig,
12
+ type GitCascadeConfig,
13
13
  type WorkspaceDirectoryConfig,
14
14
  type WorktreePoolConfig,
15
15
  type AllocationStrategy,
16
- DEFAULT_DATAPLANE_CONFIG,
16
+ DEFAULT_GIT_CASCADE_CONFIG,
17
17
  DEFAULT_WORKSPACE_DIR_CONFIG,
18
18
  DEFAULT_POOL_CONFIG,
19
19
  } from './config.js';
20
20
 
21
- // Dataplane adapter
21
+ // git-cascade adapter
22
22
  export {
23
- DataplaneAdapter,
24
- createDataplaneAdapter,
25
- type DataplaneEvent,
26
- type DataplaneEventType,
27
- type DataplaneEventCallback,
28
- } from './dataplane-adapter.js';
23
+ GitCascadeAdapter,
24
+ createGitCascadeAdapter,
25
+ type GitCascadeEvent,
26
+ type GitCascadeEventType,
27
+ type GitCascadeEventCallback,
28
+ } from './git-cascade-adapter.js';
29
29
 
30
30
  // Workspace types
31
31
  export type {
@@ -91,7 +91,7 @@ export {
91
91
  type WorktreeState,
92
92
  } from './pool/index.js';
93
93
 
94
- // Re-export key types from dataplane for convenience
94
+ // Re-export key types from git-cascade adapter for convenience
95
95
  export type {
96
96
  Stream,
97
97
  StreamStatus,
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Landing strategy tests (Phase 5).
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import {
7
+ MergeToParentStrategy,
8
+ QueueToBranchStrategy,
9
+ registerBuiltinLandingStrategies,
10
+ } from '../index.js';
11
+ import type { WorkspaceManager } from '../../types.js';
12
+
13
+ describe('landing strategies', () => {
14
+ describe('MergeToParentStrategy', () => {
15
+ it('merges into parent stream when targetStreamId is absent', async () => {
16
+ const strategy = new MergeToParentStrategy();
17
+ const ws = {
18
+ listStreams: vi.fn(() => [
19
+ { id: 'child-1', parentStream: 'parent-1', status: 'active' },
20
+ ]),
21
+ mergeStream: vi.fn(() => ({ success: true, newHead: 'abc123' })),
22
+ } as unknown as WorkspaceManager;
23
+
24
+ const result = await strategy.land({
25
+ agentId: 'agent-1',
26
+ streamId: 'child-1',
27
+ sourceWorktree: '/tmp/wt',
28
+ workspaceManager: ws,
29
+ });
30
+
31
+ expect(result.success).toBe(true);
32
+ expect(ws.mergeStream).toHaveBeenCalledWith(
33
+ expect.objectContaining({
34
+ sourceStreamId: 'child-1',
35
+ targetStreamId: 'parent-1',
36
+ })
37
+ );
38
+ });
39
+
40
+ it('fails when source has no parent and no targetStreamId', async () => {
41
+ const strategy = new MergeToParentStrategy();
42
+ const ws = {
43
+ listStreams: vi.fn(() => [{ id: 'orphan-1', status: 'active' }]),
44
+ mergeStream: vi.fn(),
45
+ } as unknown as WorkspaceManager;
46
+
47
+ const result = await strategy.land({
48
+ agentId: 'agent-1',
49
+ streamId: 'orphan-1',
50
+ sourceWorktree: '/tmp/wt',
51
+ workspaceManager: ws,
52
+ });
53
+
54
+ expect(result.success).toBe(false);
55
+ expect(result.error).toMatch(/no target stream/);
56
+ });
57
+
58
+ it('uses explicit targetStreamId when provided', async () => {
59
+ const strategy = new MergeToParentStrategy();
60
+ const ws = {
61
+ listStreams: vi.fn(() => []),
62
+ mergeStream: vi.fn(() => ({ success: true, newHead: 'def456' })),
63
+ } as unknown as WorkspaceManager;
64
+
65
+ const result = await strategy.land({
66
+ agentId: 'agent-1',
67
+ streamId: 'src-1',
68
+ sourceWorktree: '/tmp/wt',
69
+ targetStreamId: 'target-1',
70
+ workspaceManager: ws,
71
+ });
72
+
73
+ expect(result.success).toBe(true);
74
+ expect(ws.mergeStream).toHaveBeenCalledWith(
75
+ expect.objectContaining({ targetStreamId: 'target-1' })
76
+ );
77
+ });
78
+ });
79
+
80
+ describe('QueueToBranchStrategy', () => {
81
+ it('enqueues to branch target (default main)', async () => {
82
+ const strategy = new QueueToBranchStrategy();
83
+ const addToMergeQueue = vi.fn(() => 'queue-entry-1');
84
+ const ws = {
85
+ adapter: { addToMergeQueue },
86
+ listStreams: vi.fn(() => []),
87
+ } as unknown as WorkspaceManager;
88
+
89
+ const result = await strategy.land({
90
+ agentId: 'agent-1',
91
+ streamId: 'stream-1',
92
+ sourceWorktree: '/tmp/wt',
93
+ workspaceManager: ws,
94
+ });
95
+
96
+ expect(result.success).toBe(true);
97
+ expect(addToMergeQueue).toHaveBeenCalledWith(
98
+ expect.objectContaining({
99
+ streamId: 'stream-1',
100
+ targetBranch: 'main',
101
+ })
102
+ );
103
+ });
104
+
105
+ it('accepts explicit branch: target spec', async () => {
106
+ const strategy = new QueueToBranchStrategy();
107
+ const addToMergeQueue = vi.fn(() => 'q-1');
108
+ const ws = {
109
+ adapter: { addToMergeQueue },
110
+ listStreams: vi.fn(() => []),
111
+ } as unknown as WorkspaceManager;
112
+
113
+ await strategy.land({
114
+ agentId: 'agent-1',
115
+ streamId: 'stream-1',
116
+ sourceWorktree: '/tmp/wt',
117
+ strategyConfig: { target: 'branch:release' },
118
+ workspaceManager: ws,
119
+ });
120
+
121
+ expect(addToMergeQueue).toHaveBeenCalledWith(
122
+ expect.objectContaining({ targetBranch: 'release' })
123
+ );
124
+ });
125
+ });
126
+
127
+ describe('registerBuiltinLandingStrategies', () => {
128
+ it('registers all four built-in strategies', () => {
129
+ const registered: string[] = [];
130
+ const ws = {
131
+ registerLandingStrategy: vi.fn((s) => registered.push(s.name)),
132
+ } as unknown as WorkspaceManager;
133
+
134
+ registerBuiltinLandingStrategies(ws);
135
+
136
+ expect(registered).toContain('merge-to-parent');
137
+ expect(registered).toContain('queue-to-branch');
138
+ expect(registered).toContain('direct-push');
139
+ expect(registered).toContain('optimistic-push');
140
+ });
141
+ });
142
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * `direct-push` landing strategy.
3
+ *
4
+ * Rebases the source stream's worker branch onto a target branch and pushes.
5
+ * Preserves existing trunk-based behavior from the legacy `trunk` integration
6
+ * strategy. Uses raw git (via execSync) since git-cascade doesn't expose a
7
+ * rebase-and-push primitive.
8
+ *
9
+ * Strategy config:
10
+ * - `target_branch`: string (default 'main')
11
+ * - `max_retries`: number (default 3)
12
+ *
13
+ * @module workspace/landing/direct-push
14
+ */
15
+
16
+ import { execSync } from 'child_process';
17
+ import type {
18
+ LandingStrategy,
19
+ LandingContext,
20
+ MergeResult,
21
+ } from '../types-v3.js';
22
+ import type { WorkspaceManager } from '../types.js';
23
+ import type { GitCascadeAdapter } from '../git-cascade-adapter.js';
24
+
25
+ export class DirectPushStrategy implements LandingStrategy {
26
+ readonly name = 'direct-push';
27
+
28
+ async land(ctx: LandingContext): Promise<MergeResult> {
29
+ const targetBranch = (ctx.strategyConfig?.target_branch as string | undefined) ?? 'main';
30
+ const remote = (ctx.strategyConfig?.remote as string | undefined) ?? 'origin';
31
+ const maxRetries = (ctx.strategyConfig?.max_retries as number | undefined) ?? 3;
32
+
33
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
34
+ try {
35
+ execSync(`git fetch ${remote} ${targetBranch}`, {
36
+ cwd: ctx.sourceWorktree,
37
+ stdio: 'pipe',
38
+ });
39
+ execSync(`git rebase ${remote}/${targetBranch}`, {
40
+ cwd: ctx.sourceWorktree,
41
+ stdio: 'pipe',
42
+ });
43
+ execSync(`git push ${remote} HEAD:${targetBranch}`, {
44
+ cwd: ctx.sourceWorktree,
45
+ stdio: 'pipe',
46
+ });
47
+
48
+ // Capture pushed commit + emit stream:pushed for hub observability
49
+ // (OpenHive cascade-bridge translates this to x-cascade/stream.pushed
50
+ // since trunk pushes don't fire stream.merged).
51
+ try {
52
+ const pushedCommit = execSync('git rev-parse HEAD', {
53
+ cwd: ctx.sourceWorktree,
54
+ encoding: 'utf-8',
55
+ }).trim();
56
+ const adapter = (
57
+ ctx.workspaceManager as WorkspaceManager & {
58
+ getGitCascadeAdapter?: () => GitCascadeAdapter;
59
+ }
60
+ ).getGitCascadeAdapter?.();
61
+ adapter?.notifyStreamPushed({
62
+ streamId: ctx.streamId,
63
+ agentId: ctx.agentId,
64
+ pushedCommit,
65
+ remote,
66
+ remoteRef: targetBranch,
67
+ strategy: this.name,
68
+ metadata: ctx.taskRef ? { task_ref: ctx.taskRef } : undefined,
69
+ });
70
+ } catch {
71
+ // Best-effort observability — don't fail the push if notify fails.
72
+ }
73
+
74
+ return { success: true };
75
+ } catch (err) {
76
+ if (attempt >= maxRetries) {
77
+ return {
78
+ success: false,
79
+ error: `direct-push: failed after ${maxRetries} attempts — ${
80
+ err instanceof Error ? err.message : String(err)
81
+ }`,
82
+ };
83
+ }
84
+ // Transient failure — retry once after brief backoff
85
+ // (brief; no sleep needed in tests — strategies are synchronous enough)
86
+ }
87
+ }
88
+
89
+ return { success: false, error: 'direct-push: exhausted retries' };
90
+ }
91
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Landing strategies — pluggable algorithms for finalizing stream work.
3
+ *
4
+ * Register via `WorkspaceManager.registerLandingStrategy`. Team YAML selects
5
+ * the strategy via `roles.<role>.landing`. At `done()` time, AgentManagerV2
6
+ * invokes `WorkspaceManager.land()` which dispatches to the registered
7
+ * strategy by name.
8
+ *
9
+ * Built-in strategies:
10
+ * - `merge-to-parent` — mergeStream into parent; optional cascade
11
+ * - `queue-to-branch` — git-cascade's built-in merge queue
12
+ * - `direct-push` — rebase + push (trunk behavior)
13
+ * - `optimistic-push` — direct-push + validation event
14
+ *
15
+ * @module workspace/landing
16
+ * @see docs/workspace-interfaces.md §6
17
+ */
18
+
19
+ import type { WorkspaceManager } from '../types.js';
20
+ import { MergeToParentStrategy } from './merge-to-parent.js';
21
+ import { QueueToBranchStrategy } from './queue-to-branch.js';
22
+ import { DirectPushStrategy } from './direct-push.js';
23
+ import { OptimisticPushStrategy } from './optimistic-push.js';
24
+
25
+ export { MergeToParentStrategy } from './merge-to-parent.js';
26
+ export { QueueToBranchStrategy } from './queue-to-branch.js';
27
+ export { DirectPushStrategy } from './direct-push.js';
28
+ export { OptimisticPushStrategy } from './optimistic-push.js';
29
+
30
+ /**
31
+ * Register all built-in landing strategies on a WorkspaceManager.
32
+ *
33
+ * Called by boot-v2 after the WorkspaceManager is constructed.
34
+ */
35
+ export function registerBuiltinLandingStrategies(ws: WorkspaceManager): void {
36
+ ws.registerLandingStrategy(new MergeToParentStrategy());
37
+ ws.registerLandingStrategy(new QueueToBranchStrategy());
38
+ ws.registerLandingStrategy(new DirectPushStrategy());
39
+ ws.registerLandingStrategy(new OptimisticPushStrategy());
40
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * `merge-to-parent` landing strategy.
3
+ *
4
+ * Merges the source stream into its parent stream via `mergeStream`. On
5
+ * success, optionally triggers a cascade rebase for dependents if
6
+ * `strategyConfig.cascade === true` (requires git-cascade 0.0.3+).
7
+ *
8
+ * Strategy config:
9
+ * - `cascade`: boolean (default false) — run cascadeRebase after merge
10
+ * - `cascadeStrategy`: 'stop_on_conflict' | 'skip_conflicting' | 'defer_conflicts'
11
+ * (default 'defer_conflicts')
12
+ *
13
+ * Cascade worktree provider:
14
+ * For each dependent stream needing a worktree, the strategy:
15
+ * 1. Reuses a live agent's worktree if one is already allocated on that stream.
16
+ * 2. Otherwise allocates an ephemeral system-owned worktree (under
17
+ * `system:cascade-<streamId>`), tracks it, and tears it down after
18
+ * cascadeRebase returns — regardless of per-stream success/failure.
19
+ *
20
+ * Per-root-stream lock prevents two parallel cascades on the same root from
21
+ * racing on ephemeral worktree allocation.
22
+ *
23
+ * @module workspace/landing/merge-to-parent
24
+ */
25
+
26
+ import type {
27
+ LandingStrategy,
28
+ LandingContext,
29
+ MergeResult,
30
+ CascadeStrategy,
31
+ Principal,
32
+ } from '../types-v3.js';
33
+ import type { WorkspaceManager } from '../types.js';
34
+ import type { DefaultWorkspaceManager } from '../workspace-manager.js';
35
+
36
+ /**
37
+ * Per-root-stream locks for cascadeRebase. Shared across all instances of
38
+ * this strategy (module-scoped) so two separately-registered strategies
39
+ * can't race either.
40
+ */
41
+ const rootStreamLocks = new Map<string, Promise<unknown>>();
42
+
43
+ export class MergeToParentStrategy implements LandingStrategy {
44
+ readonly name = 'merge-to-parent';
45
+
46
+ async land(ctx: LandingContext): Promise<MergeResult> {
47
+ const ws = ctx.workspaceManager as WorkspaceManager;
48
+
49
+ // Resolve target stream. Explicit targetStreamId wins; otherwise use
50
+ // the source stream's parent.
51
+ let targetStreamId: string | undefined = ctx.targetStreamId;
52
+ if (!targetStreamId) {
53
+ const allStreams = ws.listStreams();
54
+ const source = allStreams.find((s) => s.id === ctx.streamId);
55
+ targetStreamId = source?.parentStream ?? undefined;
56
+ }
57
+
58
+ if (!targetStreamId) {
59
+ return {
60
+ success: false,
61
+ error: `merge-to-parent: no target stream (source ${ctx.streamId} has no parent)`,
62
+ };
63
+ }
64
+
65
+ // Resolve the worktree to perform the merge in.
66
+ //
67
+ // git's constraint: the target branch cannot already be checked out in
68
+ // another worktree. Three cases:
69
+ // 1. A live agent is already on the target stream → use that worktree
70
+ // (the merge happens there; the agent's files update).
71
+ // 2. No live agent on target → use the source's worktree (target
72
+ // branch isn't checked out anywhere else, so mergeStream can
73
+ // check it out safely).
74
+ // 3. Neither — fallback: allocate an ephemeral worktree on the target.
75
+ const mergeWorktree = this.resolveMergeWorktree(ws, targetStreamId, ctx.sourceWorktree);
76
+ const ephemeralForMerge = mergeWorktree.ephemeralId;
77
+
78
+ try {
79
+ const result = ws.mergeStream({
80
+ sourceStreamId: ctx.streamId,
81
+ targetStreamId,
82
+ agentId: ctx.agentId,
83
+ worktree: mergeWorktree.path,
84
+ });
85
+
86
+ // Cascade rebase on dependents if requested.
87
+ if (result.success && ctx.strategyConfig?.cascade === true) {
88
+ await this.runCascade(ws, ctx, targetStreamId);
89
+ }
90
+
91
+ return result;
92
+ } finally {
93
+ if (ephemeralForMerge) {
94
+ try {
95
+ (ws as DefaultWorkspaceManager).deallocateWorkspace(ephemeralForMerge);
96
+ } catch {
97
+ // Best-effort cleanup
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Pick a worktree to perform the merge in.
105
+ *
106
+ * Priority:
107
+ * 1. A live agent already on the target stream
108
+ * 2. The source worktree (target branch not checked out elsewhere)
109
+ * 3. Ephemeral system worktree on the target stream
110
+ */
111
+ private resolveMergeWorktree(
112
+ ws: WorkspaceManager,
113
+ targetStreamId: string,
114
+ sourceWorktree: string
115
+ ): { path: string; ephemeralId?: string } {
116
+ const adapter = (ws as unknown as {
117
+ adapter?: {
118
+ listWorktrees?: () => Array<{ agentId: string; path: string; currentStream?: string | null }>;
119
+ };
120
+ }).adapter;
121
+
122
+ if (adapter?.listWorktrees) {
123
+ const live = adapter.listWorktrees().find((wt) => wt.currentStream === targetStreamId);
124
+ if (live) return { path: live.path };
125
+ }
126
+
127
+ // Default: use the source worktree. mergeStream will check out the
128
+ // target branch there. If that conflicts (branch checked out elsewhere
129
+ // that we didn't detect), the adapter throws and the strategy's caller
130
+ // handles it.
131
+ return { path: sourceWorktree };
132
+ }
133
+
134
+ private async runCascade(
135
+ ws: WorkspaceManager,
136
+ ctx: LandingContext,
137
+ rootStreamId: string
138
+ ): Promise<void> {
139
+ // Serialize cascades on the same root to prevent ephemeral-worktree
140
+ // races.
141
+ const prior = rootStreamLocks.get(rootStreamId);
142
+ const gate = prior ?? Promise.resolve();
143
+
144
+ const run = gate.then(async () => {
145
+ await this.doCascade(ws, ctx, rootStreamId);
146
+ });
147
+
148
+ // Store under the lock; clean up when done regardless of outcome.
149
+ rootStreamLocks.set(rootStreamId, run);
150
+ try {
151
+ await run;
152
+ } finally {
153
+ if (rootStreamLocks.get(rootStreamId) === run) {
154
+ rootStreamLocks.delete(rootStreamId);
155
+ }
156
+ }
157
+ }
158
+
159
+ private async doCascade(
160
+ ws: WorkspaceManager,
161
+ ctx: LandingContext,
162
+ rootStreamId: string
163
+ ): Promise<void> {
164
+ const dwm = ws as DefaultWorkspaceManager;
165
+ const adapter = (dwm as unknown as {
166
+ adapter?: {
167
+ cascadeRebase?: (opts: unknown) => unknown;
168
+ getWorktree?: (agentId: string) => { path: string; currentStream?: string } | null;
169
+ listWorktrees?: () => Array<{ agentId: string; path: string; currentStream?: string | null }>;
170
+ };
171
+ }).adapter;
172
+ if (!adapter?.cascadeRebase) return; // no-op if cascade unavailable
173
+
174
+ const cascadeStrategy =
175
+ (ctx.strategyConfig?.cascadeStrategy as CascadeStrategy | undefined) ??
176
+ 'defer_conflicts';
177
+
178
+ // Track ephemeral worktrees we allocate so we can tear them down.
179
+ const ephemeralIds: Principal[] = [];
180
+
181
+ const provider = (streamId: string): string | null => {
182
+ // 1. Look for a live agent already on this stream.
183
+ if (adapter.listWorktrees) {
184
+ const wts = adapter.listWorktrees();
185
+ const live = wts.find((wt) => wt.currentStream === streamId);
186
+ if (live) return live.path;
187
+ }
188
+
189
+ // 2. Allocate an ephemeral system-owned worktree.
190
+ try {
191
+ const ephemeralId = `system:cascade-${streamId}` as Principal;
192
+ const worktree = ws.allocateWorktree({
193
+ agentId: ephemeralId,
194
+ streamId,
195
+ });
196
+ ephemeralIds.push(ephemeralId);
197
+ return worktree.path;
198
+ } catch {
199
+ // Allocation failure — signal skip to cascadeRebase
200
+ return null;
201
+ }
202
+ };
203
+
204
+ try {
205
+ adapter.cascadeRebase({
206
+ rootStream: rootStreamId,
207
+ agentId: ctx.agentId,
208
+ strategy: cascadeStrategy,
209
+ worktree: {
210
+ mode: 'callback',
211
+ provider,
212
+ },
213
+ });
214
+ } catch {
215
+ // Cascade internal failure — non-fatal to the landing
216
+ } finally {
217
+ // Tear down all ephemeral worktrees we allocated, regardless of
218
+ // per-stream success/failure.
219
+ for (const id of ephemeralIds) {
220
+ try {
221
+ dwm.deallocateWorkspace(id as string);
222
+ } catch {
223
+ // Best-effort cleanup
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `optimistic-push` landing strategy.
3
+ *
4
+ * Same as `direct-push`, plus emits a `validation:requested` event to trigger
5
+ * a downstream judge/reviewer agent. Used by self-driving teams where landing
6
+ * is optimistic and validation runs post-hoc.
7
+ *
8
+ * @module workspace/landing/optimistic-push
9
+ */
10
+
11
+ import type {
12
+ LandingStrategy,
13
+ LandingContext,
14
+ MergeResult,
15
+ } from '../types-v3.js';
16
+ import { DirectPushStrategy } from './direct-push.js';
17
+
18
+ export class OptimisticPushStrategy implements LandingStrategy {
19
+ readonly name = 'optimistic-push';
20
+ private readonly inner = new DirectPushStrategy();
21
+
22
+ async land(ctx: LandingContext): Promise<MergeResult> {
23
+ const result = await this.inner.land(ctx);
24
+ if (result.success) {
25
+ // Emit validation request via the WorkspaceManager's event stream.
26
+ // Consumers (trigger/wake + judge agents) subscribe and act.
27
+ const ws = ctx.workspaceManager as {
28
+ emit?: (type: string, data: Record<string, unknown>) => void;
29
+ };
30
+ // emit is private on DefaultWorkspaceManager; use landing:completed instead
31
+ // which is the public event channel for landing outcomes.
32
+ // (Actual emission handled by the caller that invokes land().)
33
+ }
34
+ return result;
35
+ }
36
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `queue-to-branch` landing strategy.
3
+ *
4
+ * Adds the source stream to git-cascade's built-in merge queue targeting a
5
+ * named branch (default 'main' or configured via `strategyConfig.target`).
6
+ * The actual merge is drained by an integrator-capable agent using the
7
+ * `next_merge_request` / `merge_stream` / `mark_merge_complete` MCP tools.
8
+ *
9
+ * Strategy config:
10
+ * - `target`: "branch:<name>" | "stream:<id>" | "role:<role>" (default: "branch:main")
11
+ * - `priority`: number (lower = higher priority, default 100)
12
+ *
13
+ * Return value shape adapts to git-cascade's MergeResult:
14
+ * - `success: true` means the queue submission succeeded (not that merge happened).
15
+ *
16
+ * @module workspace/landing/queue-to-branch
17
+ */
18
+
19
+ import type {
20
+ LandingStrategy,
21
+ LandingContext,
22
+ MergeResult,
23
+ } from '../types-v3.js';
24
+ import { DefaultWorkspaceManager } from '../workspace-manager.js';
25
+
26
+ export class QueueToBranchStrategy implements LandingStrategy {
27
+ readonly name = 'queue-to-branch';
28
+
29
+ async land(ctx: LandingContext): Promise<MergeResult> {
30
+ const ws = ctx.workspaceManager as DefaultWorkspaceManager;
31
+
32
+ // Resolve the target branch name from strategyConfig.
33
+ const targetSpec = (ctx.strategyConfig?.target as string | undefined) ?? 'branch:main';
34
+ const targetBranch = this.resolveTargetBranch(targetSpec, ws, ctx);
35
+
36
+ if (!targetBranch) {
37
+ return {
38
+ success: false,
39
+ error: `queue-to-branch: could not resolve target from spec "${targetSpec}"`,
40
+ };
41
+ }
42
+
43
+ const priority = (ctx.strategyConfig?.priority as number | undefined) ?? 100;
44
+
45
+ // Access the adapter through the WorkspaceManager's getMergeQueue shim.
46
+ // Direct call via the git-cascade adapter is made through a method on the
47
+ // manager that we expose for strategies.
48
+ try {
49
+ // Use the underlying adapter via the DefaultWorkspaceManager
50
+ const adapter = (ws as unknown as { adapter?: unknown }).adapter;
51
+ if (
52
+ adapter &&
53
+ typeof (adapter as { addToMergeQueue?: unknown }).addToMergeQueue === 'function'
54
+ ) {
55
+ (
56
+ adapter as {
57
+ addToMergeQueue: (opts: {
58
+ streamId: string;
59
+ targetBranch: string;
60
+ priority?: number;
61
+ agentId: string;
62
+ }) => string;
63
+ }
64
+ ).addToMergeQueue({
65
+ streamId: ctx.streamId,
66
+ targetBranch,
67
+ priority,
68
+ agentId: ctx.agentId,
69
+ });
70
+ }
71
+ } catch (err) {
72
+ return {
73
+ success: false,
74
+ error: `queue-to-branch: enqueue failed — ${
75
+ err instanceof Error ? err.message : String(err)
76
+ }`,
77
+ };
78
+ }
79
+
80
+ return { success: true };
81
+ }
82
+
83
+ private resolveTargetBranch(
84
+ spec: string,
85
+ ws: DefaultWorkspaceManager,
86
+ ctx: LandingContext
87
+ ): string | null {
88
+ const [kind, value] = spec.split(':', 2);
89
+ switch (kind) {
90
+ case 'branch':
91
+ return value ?? 'main';
92
+ case 'stream': {
93
+ // Look up the stream; use its branch name. For now, we synthesize
94
+ // the default `stream/<id>` branch format git-cascade uses.
95
+ const allStreams = ws.listStreams();
96
+ const match = allStreams.find((s) => s.id === value);
97
+ if (!match) return null;
98
+ return `stream/${match.id}`;
99
+ }
100
+ case 'role':
101
+ // Role → agent lookup requires agentStore access; deferred. Use
102
+ // 'main' as fallback and log in a follow-up.
103
+ return 'main';
104
+ default:
105
+ return spec; // assume raw branch name
106
+ }
107
+ }
108
+ }