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.
- package/CLAUDE.md +179 -38
- package/README.md +781 -131
- package/dist/acp/claude-code-replay.d.ts +11 -0
- package/dist/acp/claude-code-replay.d.ts.map +1 -0
- package/dist/acp/claude-code-replay.js +190 -0
- package/dist/acp/claude-code-replay.js.map +1 -0
- package/dist/acp/macro-agent.d.ts.map +1 -1
- package/dist/acp/macro-agent.js +155 -6
- package/dist/acp/macro-agent.js.map +1 -1
- package/dist/acp/types.d.ts +9 -0
- package/dist/acp/types.d.ts.map +1 -1
- package/dist/acp/types.js.map +1 -1
- package/dist/agent/agent-manager-v2.d.ts +21 -0
- package/dist/agent/agent-manager-v2.d.ts.map +1 -1
- package/dist/agent/agent-manager-v2.js +234 -71
- package/dist/agent/agent-manager-v2.js.map +1 -1
- package/dist/agent/agent-manager.d.ts +12 -0
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/agent/types.d.ts +15 -2
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/boot-v2.d.ts +41 -0
- package/dist/boot-v2.d.ts.map +1 -1
- package/dist/boot-v2.js +34 -37
- package/dist/boot-v2.js.map +1 -1
- package/dist/cli/index.js +56 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cognitive/macro-agent-backend.d.ts.map +1 -1
- package/dist/cognitive/macro-agent-backend.js +40 -22
- package/dist/cognitive/macro-agent-backend.js.map +1 -1
- package/dist/integrations/skilltree.d.ts.map +1 -1
- package/dist/integrations/skilltree.js +1 -0
- package/dist/integrations/skilltree.js.map +1 -1
- package/dist/lifecycle/cleanup.d.ts +33 -2
- package/dist/lifecycle/cleanup.d.ts.map +1 -1
- package/dist/lifecycle/cleanup.js +28 -6
- package/dist/lifecycle/cleanup.js.map +1 -1
- package/dist/lifecycle/handlers-v2.d.ts +7 -0
- package/dist/lifecycle/handlers-v2.d.ts.map +1 -1
- package/dist/lifecycle/handlers-v2.js +28 -2
- package/dist/lifecycle/handlers-v2.js.map +1 -1
- package/dist/lifecycle/types.d.ts +11 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/lifecycle/types.js.map +1 -1
- package/dist/map/acp-bridge.d.ts +9 -0
- package/dist/map/acp-bridge.d.ts.map +1 -1
- package/dist/map/acp-bridge.js +15 -2
- package/dist/map/acp-bridge.js.map +1 -1
- package/dist/map/cascade-bridge.d.ts +44 -0
- package/dist/map/cascade-bridge.d.ts.map +1 -0
- package/dist/map/cascade-bridge.js +257 -0
- package/dist/map/cascade-bridge.js.map +1 -0
- package/dist/map/lifecycle-bridge.d.ts +1 -8
- package/dist/map/lifecycle-bridge.d.ts.map +1 -1
- package/dist/map/lifecycle-bridge.js +76 -22
- package/dist/map/lifecycle-bridge.js.map +1 -1
- package/dist/map/server.d.ts.map +1 -1
- package/dist/map/server.js +47 -6
- package/dist/map/server.js.map +1 -1
- package/dist/map/sidecar.d.ts.map +1 -1
- package/dist/map/sidecar.js +33 -4
- package/dist/map/sidecar.js.map +1 -1
- package/dist/map/types.d.ts +20 -0
- package/dist/map/types.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.d.ts.map +1 -1
- package/dist/mcp/tools/done-v2.js +8 -0
- package/dist/mcp/tools/done-v2.js.map +1 -1
- package/dist/teams/team-manager-v2.d.ts.map +1 -1
- package/dist/teams/team-manager-v2.js +26 -0
- package/dist/teams/team-manager-v2.js.map +1 -1
- package/dist/teams/team-runtime-v2.d.ts.map +1 -1
- package/dist/teams/team-runtime-v2.js +16 -3
- package/dist/teams/team-runtime-v2.js.map +1 -1
- package/dist/workspace/config.d.ts +10 -10
- package/dist/workspace/config.d.ts.map +1 -1
- package/dist/workspace/config.js +4 -4
- package/dist/workspace/config.js.map +1 -1
- package/dist/workspace/git-cascade-adapter.d.ts +510 -0
- package/dist/workspace/git-cascade-adapter.d.ts.map +1 -0
- package/dist/workspace/git-cascade-adapter.js +908 -0
- package/dist/workspace/git-cascade-adapter.js.map +1 -0
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +4 -4
- package/dist/workspace/index.js.map +1 -1
- package/dist/workspace/landing/direct-push.d.ts +20 -0
- package/dist/workspace/landing/direct-push.d.ts.map +1 -0
- package/dist/workspace/landing/direct-push.js +74 -0
- package/dist/workspace/landing/direct-push.js.map +1 -0
- package/dist/workspace/landing/index.d.ts +29 -0
- package/dist/workspace/landing/index.d.ts.map +1 -0
- package/dist/workspace/landing/index.js +37 -0
- package/dist/workspace/landing/index.js.map +1 -0
- package/dist/workspace/landing/merge-to-parent.d.ts +41 -0
- package/dist/workspace/landing/merge-to-parent.d.ts.map +1 -0
- package/dist/workspace/landing/merge-to-parent.js +185 -0
- package/dist/workspace/landing/merge-to-parent.js.map +1 -0
- package/dist/workspace/landing/optimistic-push.d.ts +16 -0
- package/dist/workspace/landing/optimistic-push.d.ts.map +1 -0
- package/dist/workspace/landing/optimistic-push.js +27 -0
- package/dist/workspace/landing/optimistic-push.js.map +1 -0
- package/dist/workspace/landing/queue-to-branch.d.ts +24 -0
- package/dist/workspace/landing/queue-to-branch.d.ts.map +1 -0
- package/dist/workspace/landing/queue-to-branch.js +79 -0
- package/dist/workspace/landing/queue-to-branch.js.map +1 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts +10 -0
- package/dist/workspace/merge-queue/merge-queue.d.ts.map +1 -1
- package/dist/workspace/merge-queue/merge-queue.js +10 -0
- package/dist/workspace/merge-queue/merge-queue.js.map +1 -1
- package/dist/workspace/merge-queue/types.d.ts +16 -2
- package/dist/workspace/merge-queue/types.d.ts.map +1 -1
- package/dist/workspace/merge-queue/types.js +9 -0
- package/dist/workspace/merge-queue/types.js.map +1 -1
- package/dist/workspace/pool/types.d.ts +1 -0
- package/dist/workspace/pool/types.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.d.ts.map +1 -1
- package/dist/workspace/pool/worktree-pool.js +1 -0
- package/dist/workspace/pool/worktree-pool.js.map +1 -1
- package/dist/workspace/recovery/abandon.d.ts +15 -0
- package/dist/workspace/recovery/abandon.d.ts.map +1 -0
- package/dist/workspace/recovery/abandon.js +45 -0
- package/dist/workspace/recovery/abandon.js.map +1 -0
- package/dist/workspace/recovery/auto-resolve.d.ts +27 -0
- package/dist/workspace/recovery/auto-resolve.d.ts.map +1 -0
- package/dist/workspace/recovery/auto-resolve.js +99 -0
- package/dist/workspace/recovery/auto-resolve.js.map +1 -0
- package/dist/workspace/recovery/defer.d.ts +15 -0
- package/dist/workspace/recovery/defer.d.ts.map +1 -0
- package/dist/workspace/recovery/defer.js +16 -0
- package/dist/workspace/recovery/defer.js.map +1 -0
- package/dist/workspace/recovery/escalate.d.ts +16 -0
- package/dist/workspace/recovery/escalate.d.ts.map +1 -0
- package/dist/workspace/recovery/escalate.js +24 -0
- package/dist/workspace/recovery/escalate.js.map +1 -0
- package/dist/workspace/recovery/index.d.ts +32 -0
- package/dist/workspace/recovery/index.d.ts.map +1 -0
- package/dist/workspace/recovery/index.js +45 -0
- package/dist/workspace/recovery/index.js.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts +45 -0
- package/dist/workspace/recovery/spawn-resolver.d.ts.map +1 -0
- package/dist/workspace/recovery/spawn-resolver.js +111 -0
- package/dist/workspace/recovery/spawn-resolver.js.map +1 -0
- package/dist/workspace/recovery/types.d.ts +63 -0
- package/dist/workspace/recovery/types.d.ts.map +1 -0
- package/dist/workspace/recovery/types.js +12 -0
- package/dist/workspace/recovery/types.js.map +1 -0
- package/dist/workspace/topology/index.d.ts +9 -0
- package/dist/workspace/topology/index.d.ts.map +1 -0
- package/dist/workspace/topology/index.js +8 -0
- package/dist/workspace/topology/index.js.map +1 -0
- package/dist/workspace/topology/no-workspace.d.ts +18 -0
- package/dist/workspace/topology/no-workspace.d.ts.map +1 -0
- package/dist/workspace/topology/no-workspace.js +25 -0
- package/dist/workspace/topology/no-workspace.js.map +1 -0
- package/dist/workspace/topology/types.d.ts +97 -0
- package/dist/workspace/topology/types.d.ts.map +1 -0
- package/dist/workspace/topology/types.js +20 -0
- package/dist/workspace/topology/types.js.map +1 -0
- package/dist/workspace/topology/yaml-driven.d.ts +69 -0
- package/dist/workspace/topology/yaml-driven.d.ts.map +1 -0
- package/dist/workspace/topology/yaml-driven.js +273 -0
- package/dist/workspace/topology/yaml-driven.js.map +1 -0
- package/dist/workspace/types-v3.d.ts +110 -0
- package/dist/workspace/types-v3.d.ts.map +1 -0
- package/dist/workspace/types-v3.js +20 -0
- package/dist/workspace/types-v3.js.map +1 -0
- package/dist/workspace/types.d.ts +145 -17
- package/dist/workspace/types.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.d.ts +92 -13
- package/dist/workspace/workspace-manager.d.ts.map +1 -1
- package/dist/workspace/workspace-manager.js +373 -13
- package/dist/workspace/workspace-manager.js.map +1 -1
- package/dist/workspace/yaml-schema.d.ts +254 -0
- package/dist/workspace/yaml-schema.d.ts.map +1 -0
- package/dist/workspace/yaml-schema.js +170 -0
- package/dist/workspace/yaml-schema.js.map +1 -0
- package/docs/conflict-recovery.md +472 -0
- package/docs/git-cascade-integration-gaps.md +678 -0
- package/docs/workspace-interfaces.md +731 -0
- package/docs/workspace-redesign-plan.md +302 -0
- package/package.json +4 -4
- package/src/__tests__/e2e/auto-sync.e2e.test.ts +257 -0
- package/src/__tests__/e2e/cascade-rebase.e2e.test.ts +254 -0
- package/src/__tests__/e2e/cli-run.e2e.test.ts +167 -0
- package/src/__tests__/e2e/self-driving-v3.e2e.test.ts +197 -0
- package/src/__tests__/e2e/spawn-resolver.e2e.test.ts +200 -0
- package/src/__tests__/e2e/workspace-lifecycle.e2e.test.ts +30 -22
- package/src/__tests__/e2e/workspace-v3.e2e.test.ts +413 -0
- package/src/acp/__tests__/claude-code-replay.test.ts +225 -0
- package/src/acp/__tests__/macro-agent.test.ts +39 -1
- package/src/acp/claude-code-replay.ts +208 -0
- package/src/acp/macro-agent.ts +167 -9
- package/src/acp/types.ts +10 -0
- package/src/agent/__tests__/agent-manager-topology.test.ts +73 -0
- package/src/agent/__tests__/agent-manager-v2.test.ts +71 -11
- package/src/agent/__tests__/task-ref-resolution.test.ts +231 -0
- package/src/agent/agent-manager-v2.ts +293 -77
- package/src/agent/agent-manager.ts +14 -0
- package/src/agent/types.ts +16 -2
- package/src/boot-v2.ts +87 -36
- package/src/cli/index.ts +61 -0
- package/src/cognitive/__tests__/macro-agent-backend.test.ts +47 -5
- package/src/cognitive/macro-agent-backend.ts +45 -29
- package/src/integrations/skilltree.ts +1 -0
- package/src/lifecycle/cleanup.ts +52 -3
- package/src/lifecycle/handlers-v2.ts +40 -3
- package/src/lifecycle/types.ts +12 -0
- package/src/map/__tests__/cascade-bridge.test.ts +229 -0
- package/src/map/__tests__/lifecycle-bridge.test.ts +165 -22
- package/src/map/acp-bridge.ts +26 -3
- package/src/map/cascade-bridge.ts +301 -0
- package/src/map/lifecycle-bridge.ts +77 -27
- package/src/map/server.ts +47 -6
- package/src/map/sidecar.ts +31 -3
- package/src/map/types.ts +20 -0
- package/src/mcp/tools/done-v2.ts +9 -0
- package/src/teams/team-manager-v2.ts +37 -0
- package/src/teams/team-runtime-v2.ts +23 -3
- package/src/workspace/__tests__/{dataplane-adapter.test.ts → git-cascade-adapter.test.ts} +209 -14
- package/src/workspace/__tests__/self-driving-yaml.test.ts +114 -0
- package/src/workspace/__tests__/shared-worktree-refcount.test.ts +154 -0
- package/src/workspace/__tests__/standalone-mode.test.ts +118 -0
- package/src/workspace/__tests__/workspace-manager-v3.test.ts +245 -0
- package/src/workspace/__tests__/yaml-schema.test.ts +210 -0
- package/src/workspace/config.ts +11 -11
- package/src/workspace/git-cascade-adapter.ts +1186 -0
- package/src/workspace/index.ts +11 -11
- package/src/workspace/landing/__tests__/strategies.test.ts +142 -0
- package/src/workspace/landing/direct-push.ts +91 -0
- package/src/workspace/landing/index.ts +40 -0
- package/src/workspace/landing/merge-to-parent.ts +228 -0
- package/src/workspace/landing/optimistic-push.ts +36 -0
- package/src/workspace/landing/queue-to-branch.ts +108 -0
- package/src/workspace/merge-queue/merge-queue.ts +10 -0
- package/src/workspace/merge-queue/types.ts +16 -2
- package/src/workspace/pool/__tests__/worktree-pool.integration.test.ts +5 -5
- package/src/workspace/pool/types.ts +1 -0
- package/src/workspace/pool/worktree-pool.ts +1 -0
- package/src/workspace/recovery/__tests__/auto-resolve-integration.test.ts +127 -0
- package/src/workspace/recovery/__tests__/spawn-resolver.test.ts +139 -0
- package/src/workspace/recovery/__tests__/strategies.test.ts +145 -0
- package/src/workspace/recovery/abandon.ts +51 -0
- package/src/workspace/recovery/auto-resolve.ts +119 -0
- package/src/workspace/recovery/defer.ts +23 -0
- package/src/workspace/recovery/escalate.ts +30 -0
- package/src/workspace/recovery/index.ts +58 -0
- package/src/workspace/recovery/spawn-resolver.ts +145 -0
- package/src/workspace/recovery/types.ts +54 -0
- package/src/workspace/topology/__tests__/yaml-driven.test.ts +345 -0
- package/src/workspace/topology/index.ts +18 -0
- package/src/workspace/topology/no-workspace.ts +39 -0
- package/src/workspace/topology/types.ts +116 -0
- package/src/workspace/topology/yaml-driven.ts +316 -0
- package/src/workspace/types-v3.ts +155 -0
- package/src/workspace/types.ts +191 -20
- package/src/workspace/workspace-manager.ts +474 -19
- package/src/workspace/yaml-schema.ts +216 -0
- package/src/workspace/dataplane-adapter.ts +0 -546
package/src/workspace/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace Module
|
|
3
3
|
*
|
|
4
|
-
* Provides workspace isolation for agents using
|
|
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
|
|
12
|
+
type GitCascadeConfig,
|
|
13
13
|
type WorkspaceDirectoryConfig,
|
|
14
14
|
type WorktreePoolConfig,
|
|
15
15
|
type AllocationStrategy,
|
|
16
|
-
|
|
16
|
+
DEFAULT_GIT_CASCADE_CONFIG,
|
|
17
17
|
DEFAULT_WORKSPACE_DIR_CONFIG,
|
|
18
18
|
DEFAULT_POOL_CONFIG,
|
|
19
19
|
} from './config.js';
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// git-cascade adapter
|
|
22
22
|
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
} from './
|
|
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
|
|
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
|
+
}
|